c++观察者模式推荐用std::vector存储回调,避免裸指针和shared_ptr基类;注册时谨慎捕获变量,通知时浅拷贝容器防迭代器失效;提供sync/async双接口,异步需复制回调;手写比signal/slot库更轻量可控。

std::function + std::vector 怎么存回调函数
观察者模式的核心是“一堆对象等着被通知”,C++里最轻量、最可控的做法就是用 std::vector 存 std::function。别碰裸指针或 std::shared_ptr 包装的抽象基类——除非你真需要运行时多态解耦,否则纯属增加生命周期管理负担。
常见错误是把 std::function 当成普通函数指针用,结果捕获了局部变量却没检查生命周期:[&] 在异步通知中大概率导致崩溃;[=] 捕获值又容易忽略对象是否可拷贝。
- 只捕获必要项,优先用
[this]或[ptr = shared_from_this()](如果用了std::enable_shared_from_this) - 注册回调时,建议用返回值标识订阅句柄(比如
size_t索引或int64_ttoken),方便后续移除 - 避免在回调执行中途修改容器——
std::vector的erase会失效迭代器,通知循环得用下标或先收集待删索引
notify() 里怎么安全遍历并调用观察者
最典型的坑是:一边通知一边有观察者自己调用 unsubscribe(),导致迭代器失效或访问野指针。C++ 没有内置“快照式遍历”,得手动处理。
不是所有场景都需要深拷贝回调列表——如果通知是同步的、且明确禁止在回调里改订阅关系,那直接遍历原容器就行;但只要有一丁点可能(比如日志观察者写磁盘慢、触发重试逻辑),就必须隔离。
立即学习“C++免费学习笔记(深入)”;
- 推荐做法:进入
notify()后,用auto observers = m_observers;浅拷贝一份std::vector<:function>></:function>,再遍历它 - 浅拷贝开销小(
std::function内部是小对象优化+指针),比锁 + 迭代器安全方案更简单可靠 - 如果观察者数量极大(比如上万)、且通知极频繁,才考虑用
std::shared_ptr<const std::vector>></const>配合原子指针交换,但绝大多数业务用不到
如何支持跨线程通知(不崩、不丢、不卡)
观察者模式默认是同步调用,一旦某个回调阻塞或抛异常,整个通知链就停住。跨线程不是加个 std::thread 就行——核心矛盾是:谁负责生命周期?谁处理异常?消息顺序还保不保?
别用全局队列+单消费者线程来“统一派发”,这会让调用栈不可控、调试困难。真正合理的做法是让事件发布者决定分发策略。
- 提供两个通知接口:
notify_sync()(当前线程立即调用)和notify_async()(用std::async(std::launch::async, ...)或线程池投递) -
notify_async()内部必须复制回调对象(即再次浅拷贝),因为原始m_observers可能被其他线程修改 - 异步回调里别 throw 异常——
std::async不传播异常到发布者,得用std::future::wait()主动取,但通常没人这么干;更实际的是记录日志 + 继续下一个
为什么不用 signal/slot 库(如 libsigc++、Boost.Signals2)
这些库确实封装了连接管理、自动断连、线程安全等,但代价是:二进制体积增大、编译时间变长、调用栈更深、部分实现依赖 RTTI 或异常。一个只有几十行的 Subject 类,往往比引入整个 Boost 更可靠。
它们真正的适用场景是:项目已重度使用该生态、需要信号合并/限流/延迟触发等高级特性,或者团队对自研机制缺乏信心。但如果你只是想让 UI 控件响应数据变化,或模块间松耦合通信,手写更透明、更容易 debug。
- Boost.Signals2 的
scoped_connection看似优雅,但若观察者对象析构顺序不对,仍可能调用已释放内存 - libsigc++ 的模板深度高,错误信息极其晦涩,比如
sigc::slot类型不匹配时,编译器报错常跨越百行 - 手写方案里,每个
unsubscribe()就是简单的vector.erase(),哪行崩了一眼就能定位
复杂点永远在生命周期管理——不是语法难,而是你得清楚谁 new 谁 delete、谁 hold 引用、谁负责在析构前清理订阅。这点,任何库都不会替你做决定。










