std::function+std::shared_ptr无法自动注销回调,因std::function不感知shared_ptr生命周期;须用std::weak_ptr延迟校验+通知时清理,配合ScopedObserver RAII句柄实现安全观察者模式。

为什么 std::function + std::shared_ptr 组合无法自动注销回调
常见误区是把观察者句柄设计成 std::shared_ptr,以为销毁它就能通知被观察者移除回调。但问题在于:被观察者内部通常只持有 std::function 的拷贝,而 std::function 不感知外部 shared_ptr 生命周期 —— 它只在调用时检查目标是否有效(如果目标是绑定的成员函数且对象已析构,就会 UB)。所以“自动注销”必须由被观察者主动配合,不能靠回调对象单方面销毁触发。
用 std::weak_ptr 包裹回调对象并延迟校验
核心思路是:让被观察者保存一个 std::weak_ptr 到回调执行器(比如一个封装了 std::function 和生命周期控制的类),每次通知前先 lock();失败则自动清理该条目。这样既避免悬挂指针,又不依赖 RAII 的“退出作用域即注销”语义(因为观察者可能跨作用域存在)。
实操建议:
- 定义一个轻量包装类
ObserverHandle,内部持有一个std::shared_ptr<void></void>用于标记生命周期(可为空),以及一个std::function - 被观察者用
std::vector<:weak_ptr>></:weak_ptr>存储注册项 - 通知时遍历容器,对每个
weak_ptr调用lock();若返回空shared_ptr,直接erase对应项(注意迭代器失效,用索引或remove_if) - 注册时返回一个 RAII 句柄(如
ScopedObserver),其构造函数把自身shared_ptr<observerhandle></observerhandle>插入被观察者,析构函数不做任何事 —— 注销逻辑完全交给通知时的lock()校验
ScopedObserver 析构不触发注销,这是故意的
RAII 风格在这里不是“析构即注销”,而是“析构即放弃所有权,注销由被观察者按需完成”。否则会出现竞态:A 线程正在通知,B 线程析构句柄,若此时强行从容器中删除,需加锁且可能中断通知流程。延迟清理更安全、更轻量。
立即学习“C++免费学习笔记(深入)”;
使用场景注意点:
- 适用于单线程或通知/注册发生在同一线程的场景;多线程下需对被观察者的容器加读写锁(如
std::shared_mutex) - 若需要“立即注销”,应提供显式
unobserve()方法,内部调用erase并加锁 -
ScopedObserver的拷贝应禁用(= delete),移动可允许,但移动后原对象不应再参与通知逻辑
一个能跑通的最小示例骨架
以下不是完整库,而是验证核心逻辑的可运行片段(C++17):
struct EventSource {
struct ObserverHandle {
std::function<void(int)> cb;
std::shared_ptr<void> guard; // 仅用于延长生命周期
};
std::vector<std::weak_ptr<ObserverHandle>> observers;
void notify(int val) {
auto it = observers.begin();
while (it != observers.end()) {
auto sp = it->lock();
if (!sp) {
it = observers.erase(it);
} else {
sp->cb(val);
++it;
}
}
}
auto observe(std::function<void(int)> cb) {
auto handle = std::make_shared<ObserverHandle>(ObserverHandle{cb});
observers.push_back(handle);
return ScopedObserver{handle};
}
};
struct ScopedObserver {
std::shared_ptr<EventSource::ObserverHandle> handle;
ScopedObserver(std::shared_ptr<EventSource::ObserverHandle> h) : handle(h) {}
ScopedObserver(const ScopedObserver&) = delete;
ScopedObserver& operator=(const ScopedObserver&) = delete;
};
关键细节:回调是否执行,取决于 handle 是否还活着;而 ScopedObserver 的析构只是释放它持有的 shared_ptr,不碰被观察者的容器 —— 容器清理由下次 notify() 承担。
真正容易被忽略的是:如果你在回调里又调用了 observe() 或其他修改 observers 的操作,就可能引发迭代器失效或死锁。这种嵌套变更必须额外保护,不能指望基础框架自动处理。


















