应使用 std::shared_ptr 管理观察者生命周期,被观察者以 std::vector<std::weak_ptr<ObserverBase>> 存储回调,每次 notify 前调用 lock() 检查有效性,避免野指针崩溃;同时需破除 lambda 捕获 this 导致的循环引用,并在多线程下通过快照机制防止迭代器失效。

std::function + std::vector 怎么存回调而不崩溃
直接裸存 std::function 没问题,但常见崩溃是因为观察者对象提前析构,而被观察者还拿着它的回调——调用时访问了野指针。
解决思路不是“怎么存”,而是“怎么安全地存”。推荐用 std::shared_ptr 管理观察者生命周期,并配合 std::weak_ptr 在被观察者中持有弱引用:
- 观察者继承一个基类(如
ObserverBase),内部持有一个std::shared_ptr<ObserverBase>自引用 - 被观察者用
std::vector<std::weak_ptr<ObserverBase>>存储,每次触发前先lock()检查是否还活着 - 避免用裸指针或
std::unique_ptr:前者不防析构,后者无法共享所有权
示例关键片段:
void Subject::notify() {
for (auto& wptr : observers_) {
if (auto ptr = wptr.lock()) {
ptr->onEvent(data_);
}
}
}
为什么不用 Qt 的 signals/slots 做跨模块解耦
Qt 的 signal/slot 机制确实开箱即用,但它把观察者模式和 GUI 框架强绑定了。如果你的模块要跑在无 Qt 环境(比如嵌入式、服务端计算模块、测试 mock),或者需要和 Python/C 绑定层交互,就会卡住。
立即学习“C++免费学习笔记(深入)”;
更实际的问题是信号连接的隐式生命周期管理容易出错:
-
connect(obj1, &A::sig, obj2, &B::slot)默认是Qt::AutoConnection,跨线程时可能变成队列传递,导致回调延迟或重入 - 如果
obj2被delete但没显式disconnect,下次发信号就 crash - 编译期不检查参数类型匹配,错误只在运行时报
QObject::connect: Cannot queue arguments of type 'XXX'
纯 C++ 实现反而更容易控制连接/断连时机,也方便加日志、统计、调试钩子。
std::function 捕获 lambda 导致内存泄漏怎么办
这是高频坑:写 observers_.emplace_back([this]() { this->handle(); });,结果 this 是被观察者自己——形成循环引用,谁都不会析构。
根本原因是捕获了被观察者,又把它存进被观察者的容器里。破环方法很简单:
- 改用非捕获 lambda:
[&]() { handle(); }(但仅限函数内联调用,不能存) - 改用普通函数指针或静态成员函数(无状态)
- 最通用:让观察者自己提供回调,由它管理捕获内容,被观察者只存
std::function<void()>,不参与捕获逻辑
例如观察者暴露一个 getCallback() 接口,返回已绑定好上下文的 std::function,被观察者只负责调用,不构造。
多线程下 notify() 怎么避免迭代器失效和重复通知
一边在 notify() 遍历 std::vector,一边其他线程调用 register()/unregister() —— 迭代器立刻失效,push_back 或 erase 都可能触发 realloc。
别用读写锁硬扛,性能差还容易死锁。更轻量的做法是:
- 注册/注销操作走原子队列(如
std::queue<std::shared_ptr<ObserverBase>>+std::mutex),只在notify()开始前快照一次当前有效观察者列表 - 用
std::vector<std::weak_ptr<...>>+reserve()预分配,避免 notify 中 realloc - 禁止在回调里调用
register/unregister;如需动态调整,改为 post 到事件队列异步处理
真正麻烦的不是并发本身,而是回调里再触发新事件——容易形成不可控的调用链。这点比线程安全更值得盯紧。











