C++观察者模式推荐用std::function+std::vector实现,避免虚函数继承体系带来的耦合、单继承限制和move-only类型支持问题,通过RAII(如ObserverGuard)和weak_ptr确保生命周期安全。

用 std::function + std::vector 实现轻量观察者
观察者模式在 C++ 里不需要硬套 GoF 的抽象类继承结构——那反而容易把逻辑锁死、增加头文件依赖。更实际的做法是用类型擦除(std::function)存回调,用 std::vector 管理订阅者列表。
常见错误是直接存裸指针或 this,导致观察者析构后 Subject 还调用已释放内存,触发 segmentation fault 或未定义行为。
- 观察者注册时,优先传入 lambda(捕获
weak_ptr或值拷贝),避免生命周期隐患 - Subject 内部不持有观察者的
shared_ptr,只负责调用;谁管理生命周期,由观察者自己决定 - 遍历时不要边迭代边
erase,先收集待移除的索引,再倒序删除,或改用std::erase_if(C++20)
class Subject {
std::vector<std::function<void(int)>> observers_;
public:
void attach(std::function<void(int)> cb) {
observers_.push_back(std::move(cb));
}
void notify(int value) {
for (auto& cb : observers_) cb(value); // 不检查 cb 是否为空
}
};
为什么不用虚函数接口实现 Observer?
纯虚基类(如 class Observer { virtual void onEvent(int) = 0; })看似“标准”,但实际带来三处硬伤:头文件强耦合、单继承限制、以及最要命的——调用链上无法做 move-only 捕获(比如想在回调里移动一个 std::unique_ptr)。
使用 std::function 后,任何可调用对象都能注册:lambda、绑定函数、仿函数,甚至其他类的成员函数(配合 std::bind 或捕获 this)。
立即学习“C++免费学习笔记(深入)”;
- 虚函数方案要求所有观察者继承同一基类,而真实项目中 UI 组件、日志模块、网络层往往完全无关,不该为模式强行拉通继承体系
-
std::function构造时会复制或移动捕获变量,天然支持 move-only 类型;虚函数接口只能传 const 引用或指针,没法移交所有权 - 性能上差异极小:现代编译器对
std::function调用常能内联,虚函数调用则必然有一次间接跳转
如何安全处理观察者提前注销?
典型场景:UI 控件注册了事件回调,但控件被销毁时忘了 detach,后续通知就 crash。这不是模式问题,是资源管理没对齐。
最稳妥的方式不是让 Subject 去“检测”观察者是否存活,而是让观察者自己控制生命周期——用 RAII 封装注册/注销动作。
- 写一个
ObserverGuard类,在构造时attach,析构时detach,把它作为成员变量放在观察者类里 - 避免在 lambda 中直接捕获
this,改用std::weak_ptr<Self>,并在回调开头用lock()判断是否还活着 - Subject 不该提供“按地址查找并删除”的接口(易误删、线程不安全),注销应通过唯一 token 或 guard 对象触发
std::function 的开销和替代选项
std::function 确实有小对象优化(SOO)之外的堆分配可能,但日常业务逻辑中这点开销远小于一次信号 emit 的实际工作量。真到性能敏感场景(比如每帧调用数千次),才需要换方案。
- 高频短回调(如粒子系统更新):改用函数指针 +
void*用户数据,零开销但丧失类型安全 - 需支持多线程并发注册/通知:
std::vector得加锁,或换std::shared_mutex(C++17)保护读多写少场景 - 不想依赖 STL 容器:用静态数组 + 计数器,上限固定但无内存分配,适合嵌入式
真正容易被忽略的点是:通知顺序。STL 容器遍历是确定顺序,但如果你用了哈希表或并发容器,顺序就不可靠——而很多业务逻辑(比如 UI 刷新、状态机流转)隐式依赖通知先后。










