std::function + std::shared_ptr 组合易致循环引用,因观察者注册时通过 std::bind 捕获 this(强引用被观察者),而被观察者又用 shared_ptr 管理观察者列表,形成双向强引用链,导致双方引用计数永不归零、无法析构。

为什么 std::function + std::shared_ptr 组合容易导致循环引用?
观察者注册后长期持有被观察者的 shared_ptr(比如通过 std::bind 捕获 this),而被观察者又用 shared_ptr 管理观察者列表,就会形成双向强引用——双方都无法析构。这不是“忘了删 observer”,而是 RAII 机制本身在推着你掉进坑里。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 观察者注册时,**只传入
std::weak_ptr或裸指针 + 生命周期保证**,不参与所有权管理 - 被观察者内部用
std::vector<:weak_ptr>></:weak_ptr>存储,每次通知前调用lock()检查有效性 - 若必须绑定成员函数,优先用
[this_weak = weak_from_this()]() mutable { if (auto self = this_weak.lock()) { self->on_event(); } }模式
如何让回调自动注销而不依赖手动调用 unregister()?
核心是把“注销时机”绑定到观察者对象的生命周期末尾,而不是靠用户记着调 unregister()。C++17 起可借助 std::shared_ptr 的自定义删除器(deleter)实现。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 被观察者提供一个
register_observer(std::shared_ptr<observer> obs)</observer>接口 - 在注册时,为该
shared_ptr设置 deleter:捕获被观察者持有的管理句柄(如std::list<Observer*>::iterator或 ID),在 deleter 中执行清理 - 更轻量的做法:观察者构造时接收一个
std::function<void> cleanup_hook</void>,在析构中直接触发——前提是被观察者能提前注册好这个 hook
示例关键片段:
void Subject::register_observer(std::shared_ptr<Observer> obs) {
auto it = observers_.insert(observers_.end(), obs.get());
// 把迭代器 move 进 deleter,确保 observer 析构时自动从容器移除
obs.reset(obs.get(), [this, it](Observer*) mutable {
observers_.erase(it);
});
}
std::list vs std::vector 存储观察者指针,哪个更适合自动注销场景?
std::list 支持 erase(iterator) 后 iterator 仍有效(对其他元素),且 erase 不会使其他迭代器失效;而 std::vector erase 后后续所有迭代器/引用都可能失效——这对“在遍历通知过程中某个 observer 自行析构并触发注销”非常危险。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 选
std::list<Observer*>或std::list<std::weak_ptr<Observer>>,避免遍历时因 erase 导致迭代器崩溃 - 若坚持用
vector,必须先收集待通知的有效 observer(如用std::vector<std::shared_ptr<Observer>>临时拷贝),再遍历该副本 - 注意:裸指针需确保 observer 对象生存期严格长于 subject,否则
lock()失败或解引用空指针
RAII 风格观察者最易忽略的边界:多线程与异常安全
自动注销只解决“对象销毁时清理”,但没解决“通知中途 observer 被销毁”或“通知抛异常导致部分 observer 没收到”。RAII 不等于线程安全,也不等于异常中立。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 若 subject 可能被多线程调用,observer 列表访问必须加锁(
mutable std::shared_mutex比std::mutex更适合读多写少) - 通知循环中每个
obs->update()应包在try/catch(...)内,防止一个 observer 崩溃中断整个通知链 - 不要在 deleter 或 destructor 中做耗时操作(如网络调用、锁竞争),它可能在任意线程触发
真正轻量的关键,不是代码行数少,而是把“谁负责释放”“何时释放”“释放时能否出错”这三件事,在类型系统和生命周期契约里钉死。稍一松动,自动注销就变成自动泄漏或自动 crash。


















