直接用 std::function + std::vector 注册回调易导致野指针崩溃,因生命周期完全依赖使用者;RAII 应通过 ObserverHandle(移动安全、析构自动解绑)绑定回调与观察者生存期,避免 shared_ptr/weak_ptr 开销。

为什么直接用 std::function + std::vector 注册回调容易出问题
因为生命周期管理完全甩给使用者:观察者对象销毁了,但回调还留在被观察者里,下次触发就 std::bad_function_call 或野指针崩溃。RAII 的核心不是“自动释放”,而是“绑定生存期”——让回调的存续明确依附于某个对象的生命周期。
常见错误现象:Segmentation fault 出现在第 3 次通知后;调试发现 std::function 内部保存的是已析构对象的 this 指针;weak_ptr 方案又引入虚表和引用计数开销,不轻量。
- 避免裸指针或
shared_ptr持有观察者:破坏 RAII 的确定性析构 - 拒绝在被观察者中维护“弱引用池”:增加复杂度且仍需手动清理
- 关键思路:把“注册”动作变成一个拥有析构行为的对象(即句柄),由它负责反注册
ObserverHandle 必须支持移动、不可复制、析构即解绑
这是 RAII 封装成败的关键。句柄本身不持有业务逻辑,只记下被观察者的注销接口(函数指针或 lambda)和唯一 ID(用于快速查找)。它的析构必须无条件触发一次解绑。
示例结构简写:
立即学习“C++免费学习笔记(深入)”;
class ObserverHandle {
std::function<void()> m_unsubscribe;
public:
explicit ObserverHandle(std::function<void()> unsub)
: m_unsubscribe(std::move(unsub)) {}
ObserverHandle(ObserverHandle&&) noexcept = default;
ObserverHandle(const ObserverHandle&) = delete;
ObserverHandle& operator=(const ObserverHandle&) = delete;
ObserverHandle& operator=(ObserverHandle&&) noexcept = default;
~ObserverHandle() { if (m_unsubscribe) m_unsubscribe(); }
};
注意:m_unsubscribe 必须是可调用对象,不能是 raw function pointer(无法捕获上下文),也不能是带状态的 std::function 以外的类型(兼容性差)。
- 移动构造/赋值必须
noexcept,否则容器操作(如std::vector::push_back)可能异常中止导致泄漏 - 析构中判空再调用,防止多次移动后重复解绑
- 不要在句柄里存
this或观察者引用——那是使用者的责任
被观察者端用 std::vector<std::function<void()>> 最简可行,但要注意迭代器失效
不需要哈希表或红黑树——轻量级场景下,顺序遍历 + 索引擦除足够快;且避免引入额外内存分配和比较开销。问题在于:通知过程中若某回调触发自身解绑,erase 会让后续迭代器失效。
解决方式不是锁或延迟删除,而是“反向遍历 + 下标访问”:
for (size_t i = m_callbacks.size(); i > 0; --i) {
if (m_callbacks[i-1]) { // 防空回调
m_callbacks[i-1]();
}
}
这样即使中间某次调用导致 m_callbacks 缩容,也不会影响前面的索引。
- 回调存储用
std::vector<std::function<void()>>,不用std::list:后者每次插入都 new,违背“轻量”目标 - 注册返回
ObserverHandle,内部unsubscribelambda 捕获this和当前索引(或迭代器),执行erase即可 - 不提供“按名称/类型解绑”接口:增加泛型复杂度,且违背“句柄即生命周期”的设计契约
使用时最容易忽略:lambda 捕获方式决定是否安全
你写的这个 lambda,是不是用了 [this]?如果 this 指向栈对象或即将析构的对象,句柄还在,回调一触发就崩。RAII 只管“谁来解绑”,不管“绑的是什么”。
正确姿势只有两种:
- 观察者是堆对象(
new或std::make_unique),且保证生命周期长于被观察者 → 用[this] - 观察者是栈对象,必须用
[&local_var]捕获局部变量,或更推荐:把回调逻辑封装进成员函数,再用[this]绑定该函数(前提是 this 安全)
典型翻车代码:auto h = subject.observe([this]{ onEvent(); }); —— 若 this 是临时作用域里的对象,这行之后句柄就已悬空。
真正轻量的核心,是把生命周期责任推回给使用者,而不是在框架层加一堆运行时检查或智能指针开销。一旦接受这个前提,实现就非常干净。


















