std::function + std::shared_ptr 不适合轻量级观察者,因其每次注册隐式堆分配且原子引用计数开销大;RAII 观察者应使用栈对象+索引句柄+空洞向量实现无锁、无原子操作的高效生命周期管理。

为什么 std::function + std::shared_ptr 组合不适合轻量级观察者?
因为每次注册回调都隐式分配堆内存,且引用计数原子操作在高频事件中开销明显。RAII 风格的观察者必须避免运行时堆分配,同时保证生命周期安全——这意味着观察者对象(Observer)本身应是栈对象,而被观察者(Subject)不能持有其强引用。
常见错误现象:std::weak_ptr 检查后调用回调时仍触发崩溃,本质是 weak_ptr.lock() 成功但对象已被析构(析构函数未及时从列表中移除)。
- 正确做法:让
Observer在构造时向Subject注册一个轻量句柄(如size_t索引),析构时自动反注册 - 关键约束:
Subject内部存储回调不存裸指针,也不存shared_ptr,而是用std::vector<:function>></:function>+ 空洞标记(std::nullopt)管理 - 性能影响:插入/删除均摊 O(1),无锁,无原子操作;但遍历通知时需跳过空洞,实际吞吐取决于空洞密度
Observer 构造即注册、析构即解绑的 RAII 实现要点
核心是把“注册-反注册”逻辑下沉到 Observer 的生命周期中,而非暴露给用户手动调用。这要求 Subject 提供一个非侵入式注册接口,返回可唯一标识该观察者的句柄。
使用场景:UI 事件监听、配置变更通知、传感器数据订阅等短生命周期、高频率回调场景。
立即学习“C++免费学习笔记(深入)”;
-
Observer构造函数接收Subject&和回调 lambda,立即调用subject.subscribe([this]{ ... })并保存返回的size_t索引 - 析构函数内调用
subject.unsubscribe(handle_),将对应位置置为std::nullopt - 必须禁止拷贝(
= delete),允许移动(转移句柄所有权),否则多个Observer实例可能指向同一索引导致重复反注册 - 示例片段:
class Observer {<br> Subject& subj_;<br> size_t handle_;<br>public:<br> Observer(Subject& s, auto&& cb) : subj_(s), handle_(s.subscribe(std::forward<decltype(cb)>(cb))) {}<br> Observer(const Observer&) = delete;<br> Observer& operator=(const Observer&) = delete;<br> Observer(Observer&& o) noexcept : subj_(o.subj_), handle_(std::exchange(o.handle_, SIZE_MAX)) {}<br> ~Observer() { if (handle_ != SIZE_MAX) subj_.unsubscribe(handle_); }<br>};
Subject::notify() 如何安全遍历并跳过已失效回调?
不能边遍历边 erase(会失效迭代器),也不能用 std::vector::erase(std::remove_if(...)) —— 那会引发不必要的内存搬移。最简方案是预分配一个临时 vector 存储待执行回调,再批量调用。
容易踩的坑:lambda 捕获了局部变量,但通知发生在异步线程中,导致 UAF;或回调内部又调用了 subscribe/unsubscribe,造成容器重入修改。
- 推荐做法:遍历原始
callbacks_(std::vector<:optional>>></:optional>),对每个非空项 push 到临时std::vector<:function>></:function>,再 for-each 调用 - 不支持递归通知:在
notify()执行期间禁止调用subscribe/unsubscribe,可通过bool notifying_{false}+ 断言捕获 - 参数差异:若需传递事件参数,回调类型应为
std::function<void event></void>,notify接收Event&&并完美转发 - 兼容性注意:C++17 起
std::optional可用;若限 C++11,可用std::unique_ptr+reset()模拟空状态,但失去 trivial destructibility
为什么不用 std::signal 或 Boost.Signals2?
它们功能完备但重量级:boost::signals2 默认带线程安全、序列化、连接管理等,编译依赖大,二进制体积膨胀明显;std::signal(C 标准库)仅支持全局信号,无法绑定对象上下文。
真实需求往往是:一个类成员函数要响应另一个类的状态变化,且两者生命周期独立——这时手写轻量版反而更可控、更易调试、更少意外行为。
- 复杂点在于:必须显式处理“谁拥有回调所有权”和“谁负责清理”。RAII 观察者把所有权交给
Observer实例,清理动作绑定在其析构,这是最贴近直觉的设计 - 容易被忽略的地方:若
Subject生命周期短于Observer,Observer析构时访问已销毁的Subject会导致 crash。解决方法是在Subject析构前强制清空所有句柄(遍历 callbacks_ 置空),并让unsubscribe对无效句柄静默失败


















