用 std::function 存回调、std::shared_ptr+自定义删除器管理订阅,可安全解决观察者提前析构导致的悬空回调问题;裸指针或引用必然引发未定义行为,weak_ptr+lock() 和 RAII 句柄是关键实践。

直接说结论:用 std::function 存回调、用 std::shared_ptr + 自定义删除器管理订阅生命周期,比手写引用计数或裸指针安全得多,且 RAII 能自然解决“观察者提前析构导致回调悬空”的核心痛点。
为什么裸指针或 raw reference 在 observer 中必然出问题
常见错误是让 subject 持有 Observer* 或 Observer&,一旦 observer 对象先于 subject 销毁,后续 notify 就触发未定义行为——典型报错如 EXC_BAD_ACCESS(macOS)或 access violation(Windows)。这不是“偶尔出错”,而是只要 observer 是栈对象或提前 delete,就必崩。
实操建议:
- 永远不存裸指针或引用到可能短生命周期的对象
- 如果 observer 必须是栈对象(比如局部 lambda),必须保证其生命周期 ≥ subject 生命周期,否则只能转用
std::shared_ptr包裹 - 用
std::weak_ptr做“弱持有”是更稳妥的选择,但需在notify时先lock()判断是否还活着
std::function 回调的参数设计与性能取舍
传值还是传 const 引用?是否允许移动捕获?这直接影响回调开销和使用灵活性。例如 std::function<void event></void> 支持所有捕获方式,而 std::function<void></void> 会强制拷贝 event,对大对象很伤。
立即学习“C++免费学习笔记(深入)”;
实操建议:
- 事件类型
Event应为轻量级(如仅含 id、timestamp、少量字段),避免在回调中隐式拷贝大对象 - 回调签名统一用
const Event&,既避免拷贝,又支持 lambda 捕获值或引用 - 若需异步通知,不要在
std::function里做耗时操作;可由调用方决定是否投递到线程池
RAII 订阅句柄:用 std::shared_ptr + 自定义删除器自动退订
手动调用 unsubscribe() 容易遗漏,尤其在异常路径或早期 return 场景。RAII 的解法是返回一个“句柄”对象,在其析构时自动清理注册关系。
实操建议:
- 订阅函数返回
std::shared_ptr<void></void>,构造时绑定自定义删除器:调用 subject 内部的remove_observer方法 - 删除器必须捕获 subject 的
weak_ptr或原始指针 + 标志位,防止 subject 已销毁还尝试访问 - 示例关键片段:
auto handle = std::shared_ptr<void>(nullptr, [subject_ptr, token](void*) { if (auto s = subject_ptr.lock()) s->unregister(token); });
多线程下 notify 的最小安全边界
observer 模式本身不解决线程安全;std::function 调用不是原子的,多个线程同时 notify 可能导致回调执行顺序混乱,甚至迭代器失效(如果用 vector 存回调并边遍历边删)。
实操建议:
- 回调容器用
std::vector<:weak_ptr event>>></:weak_ptr>,遍历时先lock()得到临时shared_ptr,再调用;避免边遍历边 erase - notify 不加锁也能工作,但需接受“某次通知可能丢失”——适用于日志、监控等非关键路径
- 若要求强一致性(如状态同步),必须对回调容器加
std::shared_mutex(读多写少)或std::mutex(简单起见)
最易被忽略的一点:observer 的回调函数里调用 subject 的任何方法(包括 unsubscribe)都可能引发重入,导致容器迭代器失效或重复析构。务必把“正在通知中”设为标志位,或改用快照拷贝(copy-on-write)方式遍历回调列表。


















