std::function+std::vector可高效实现轻量观察者模式,避免虚函数开销和循环引用;推荐用lambda订阅、延迟移除、weak_ptr安全调用、operator+=/()提升API自然度,多线程下宜用shared_ptr快照语义。

std::function + std::vector 足够应付大多数场景
观察者模式在 C++ 里不需要硬套 GoF 教科书写法。用 std::function 存回调、std::vector 存观察者,既轻量又灵活,编译期开销低,也避免虚函数带来的间接调用成本。
常见错误是把观察者设计成带状态的类对象,再用 std::shared_ptr 管理——结果引入循环引用或生命周期失控。其实多数事件只需“收到就执行”,连类都不必定义。
- 订阅时用
std::function,支持 lambda、普通函数、成员函数(绑定后) - 触发时遍历 vector 调用,顺序即注册顺序;如需保证执行顺序稳定,别依赖插入顺序,显式排序或用
std::map - 避免在回调里调用
unsubscribe():会导致 vector 迭代器失效。改用“延迟移除”机制(例如标记待删,触发完统一清理)
std::weak_ptr 防止悬挂指针,但不是万能解药
当观察者必须是某个对象的成员函数,且该对象生命周期不确定时,std::weak_ptr 是标准做法。但它只解决“调用前检查是否还活着”,不解决“调用中对象被析构”的竞态问题——尤其在多线程下。
典型误用:直接在 lambda 里捕获 shared_ptr 并调用成员函数,却没意识到回调可能跨线程执行,而对象可能已在另一线程析构。
立即学习“C++免费学习笔记(深入)”;
- 正确姿势:lambda 捕获
weak_ptr,进入回调第一行调用lock();返回空shared_ptr就直接 return - 不要在
lock()后长期持有shared_ptr,尤其不能把它存到全局或传给异步任务——这会意外延长对象寿命 - 单线程场景下,若能确保观察者生命周期 ≥ 被观察者,用原始指针 + 注册/注销配对更高效,省去智能指针开销
operator+= 和 operator() 重载让 API 更自然
用户不关心你内部用 vector 还是 map,只希望写 subject += [](auto&& e) { ... }; 或 subject(event);。重载这两个操作符能让接口贴近直觉,也减少模板参数暴露。
容易踩的坑是把 operator+= 设为模板函数却不加约束,导致匹配到奇怪类型(比如 int),编译报错信息极长。或者 operator() 不做空观察者检查,一触发就崩。
- 用
std::is_invocable_v限定operator+=的参数类型 -
operator()内部先判空:if (observers_.empty()) return;,避免无意义循环 - 别为了“看起来像信号槽”而重载
operator-=做精确匹配——函数对象无法比较相等,只能靠唯一 ID 或 erase-remove 惯用法
多线程下 notify 必须加锁,但锁粒度影响性能
观察者列表读多写少,典型场景是频繁触发、偶尔增删。用 std::mutex 全局锁最简单,但会成为瓶颈;用读写锁(如 std::shared_mutex)能提升并发度,但 C++17 前不原生支持,且写锁仍阻塞所有通知。
更现实的选择是:读路径无锁(copy-on-write 或 RCU 风格),写路径锁保护。但别自己手写 RCU——容易出错,可用 std::shared_ptr<:vector>> 实现快照语义。
- notify 时先原子读取当前
std::shared_ptr,再遍历副本,完全不碰锁 - subscribe/unsubscribe 时锁住写操作,替换整个 vector 的 shared_ptr
- 注意:副本遍历期间新增的观察者本次收不到事件,这是合理取舍;若要求“绝对不漏”,就得接受锁阻塞
真正难的不是怎么加锁,而是想清楚“事件丢失是否可接受”。很多业务逻辑其实容忍一次漏发,但没人愿意为它多写 200 行线程安全代码。










