std::function + std::vector 足以实现轻量、类型安全、无内存泄漏的观察者模式,无需GoF类图;核心是支持注册、调用与解绑,配合 weak_ptr 和 erase_if 可安全处理生命周期与中途移除问题。

为什么 std::function + std::vector 就够用了
观察者模式在 C++ 里不必强行套用经典 GoF 类图。现代 C++ 的 std::function 和 std::vector 足以支撑轻量、类型安全、无内存泄漏的通知机制,且避免虚函数开销和继承耦合。
关键不是“实现模式”,而是“让通知可注册、可调用、可解绑”。下面所有设计都围绕这个目标展开:
-
std::function封装任意可调用体(lambda、成员函数指针、普通函数),类型擦除但编译期检查参数匹配 - 用
std::vector存储观察者,简单直接;若需频繁增删查,可换std::unordered_map配唯一 ID,但多数场景没必要 - 不依赖基类或接口,观察者无需继承、无需实现特定函数,降低侵入性
如何安全绑定成员函数并避免悬空调用
成员函数指针必须绑定对象实例,而对象生命周期常早于事件源。最常见错误是捕获局部对象或已析构对象的 this 指针,导致未定义行为。
推荐做法:统一用 std::shared_ptr 管理被观察对象,并要求观察者也通过 std::weak_ptr 持有引用 —— 这样既支持自动解绑,又不延长对象寿命:
立即学习“C++免费学习笔记(深入)”;
class Subject {
public:
using Callback = std::function;
void attach(Callback cb) { observers_.push_back(cb); }
void notify(int value) {
for (auto& cb : observers_) cb(value);
}
private:
std::vector observers_;
};
struct Observer {
explicit Observer(std::sharedptr sub)
: subject (sub) {
// 绑定时捕获 weakptr,调用前 lock()
subject->attach([wp = std::weak_ptr(shared_from_this())](int v) {
if (auto self = wp.lock()) {
self->onEvent(v);
}
});
}
void onEvent(int v) { / ... / }
std::shared_ptr shared_from_this() { return shared_from_this(); }
std::sharedptr subject ;
};
std::erase_if + std::shared_ptr 解决通知中途移除观察者的问题
通知过程中,某个回调可能调用 detach(),直接 erase 迭代器会破坏循环。传统方案用两遍遍历(标记+清理)或手动维护索引,繁琐易错。
C++20 起,std::erase_if 是更干净的解法:先收集待移除项,再批量擦除。配合 std::shared_ptr 的引用计数,还能自然处理“通知中 self-destruct”场景:
- 在
notify()前复制一份observers_,确保迭代安全 - 每个回调内部若调用
detach(),只标记自身失效,不立即修改原容器 - 通知结束后,用
std::erase_if(observers_, [](auto& cb) { return cb.expired(); })清理(若用std::weak_ptr包裹回调)
若不用弱引用,也可用 token 机制:每次 attach() 返回一个 std::size_t id,detach(id) 标记该 id 失效,notify() 跳过失效项。
事件委托与跨线程通知的边界在哪
纯内存内通知(同一线程)不需要锁或队列。一旦涉及多线程,就不再是“观察者模式”的范畴,而是“事件总线”或“消息泵”的职责。
常见误用:在 UI 线程注册回调,却从后台线程触发 notify(),导致 GUI 操作崩溃。此时必须明确约定:
- 事件源不负责线程调度 —— 由使用者决定是否投递到主线程(如 Qt 的
QMetaObject::invokeMethod,或 Windows 的PostMessage) - 若需异步通知,应显式封装为
std::thread或std::async,并在回调内做线程安全判断(例如检查QThread::currentThread() == ui_thread_) - 不要在
std::function容器里存裸函数指针 +void*用户数据 —— 这种 C 风格写法丢失类型信息,且无法自动管理生命周期
真正难的从来不是怎么发通知,而是谁来保证“发出去之后,接收方还活着、还在正确线程、还没被用户关掉窗口”。这部分逻辑必须外提,不能塞进 Subject 类里。











