用c++17可基于std::function+std::vector+shared_ptr实现轻量链式观察者:深拷贝赋值、弱捕获防循环引用、移动构造优化性能,回调执行前拷贝列表避免迭代失效。

如何用 C++17 实现轻量级可链式调用的观察者
直接结论:不用第三方库,靠 std::function + std::vector + 移动语义就能搭出响应式内核,关键在避免裸指针生命周期失控和回调重复注册。
典型场景是 UI 状态变更通知、配置热更新监听、事件总线底层。不是要复刻 RxCpp,而是让一个 Value<int></int> 被修改时,自动触发一串 std::function<void></void> 回调,且支持链式注册(.on_change([]{}).on_change([]{}))。
- 用
std::shared_ptr<:vector>>></:vector>存储回调,避免观察者析构后容器里还留着悬垂函数对象 - 注册接口返回
*this,但必须是Value&而非Value&&,否则链式调用在临时对象上会崩 - 触发回调时用范围 for 遍历,不要边遍历边 erase —— 某些回调内部可能调用
unobserve(),得先拷贝一份回调列表再执行
为什么 operator= 重载必须深拷贝观察者列表
现象:auto a = value; auto b = value; 后修改 a,b 的回调也触发了 —— 这是浅拷贝共享了同一个 std::shared_ptr 导致的误触发。
正确做法是:赋值时创建新的 std::shared_ptr,把原回调列表的内容 move 进去。否则两个变量看似独立,实则共用一套监听器,违背响应式“每个实例自治”的直觉。
- 不重载
operator=?默认行为就是共享shared_ptr,绝对不行 - 只拷贝函数对象本身(
std::function是可拷贝的),不要试图拷贝捕获的上下文 —— 那是用户责任 - 如果真需要共享监听逻辑,应显式传入同一
std::shared_ptr<:vector>></:vector>,而不是靠赋值隐式共享
std::function 捕获 this 导致循环引用怎么办
错误写法:value.on_change([this]{ this->update_ui(); }); —— 此时 Value 持有该 std::function,std::function 又持有 this,导致 Value 永远无法析构。
解法只有两个,且必须二选一:
- 用弱捕获:
[w = weak_from_this()] { if (auto p = w.lock()) p->update_ui(); }(要求Value继承自std::enable_shared_from_this) - 改用原始指针 + 注册时约定生命周期:
[ptr = this] { ptr->update_ui(); },并在Value析构前手动清空回调列表(比如加个clear_observers()) - 绝对不要依赖“我保证 this 活得比 Value 久”——这种假设在重构或组合使用时极易破防
性能瓶颈常出现在频繁触发的 on_change 场景
当一个 Value<double></double> 每毫秒被赋值一次,而它绑了 50 个回调,每次触发都要遍历 vector + 调用 std::function::operator(),开销会明显上升。
优化点很实际:
- 把
std::vector<:function>></:function>换成std::vector<void></void>+ 用户传函数指针(牺牲灵活性换 2~3 倍调用速度) - 加个开关字段
bool m_enabled = true;,set_value()开头先检查,避免无谓遍历 - 如果回调中大量做 I/O 或锁操作,考虑用
std::async异步派发 —— 但注意:这会让执行顺序不可预测,慎用于状态同步逻辑
最常被忽略的是:没有为 Value 提供移动构造函数。一旦放进 std::vector<value>></value>,默认拷贝会复制整个回调列表,而移动本可以只 transfer shared_ptr 内部指针。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











