最稳妥的组合是因raii句柄在析构时自动退订,避免悬空回调;std::function需配合shared_ptr或weak_ptr管理被观察对象生命周期,防止use-after-free。

为什么 std::function + RAII 是观察者注销最稳妥的组合
手动管理观察者生命周期容易导致悬空回调,而 std::function 本身不持有被调用对象的所有权,必须配合 RAII 自动清理。关键在于:**订阅时返回一个可析构的句柄,析构即自动退订**——这比“注册时传裸指针+手动调 unregister”可靠得多。
常见错误现象:std::function 捕获了 this 后,被观察对象已销毁,但观察者列表里仍存活着一个调用会 crash 的函数对象。
- 必须用
std::shared_ptr或std::weak_ptr管理被观察对象生命周期(若观察者需访问其状态) - 句柄类型不能是裸指针或引用;推荐用轻量级结构体封装
std::list<...>::iterator</...>或原子计数器 ID - 避免在回调执行期间修改观察者容器(如边遍历边 erase),否则迭代器失效
如何设计可自动注销的订阅句柄(RAII 核心)
句柄本质是一个作用域绑定的“退订令牌”。典型做法是让 subscribe() 返回一个 ObserverHandle 对象,其析构函数调用内部 unsubscribe() 逻辑。
示例关键结构:
struct ObserverHandle {
std::list<:function>>* list_ = nullptr;
std::list<:function>>::iterator it_;
<pre class="brush:php;toolbar:false;">ObserverHandle(std::list<std::function<void()>>& l,
std::list<std::function<void()>>::iterator i)
: list_(&l), it_(i) {}
~ObserverHandle() {
if (list_) list_->erase(it_);
}
ObserverHandle(const ObserverHandle&) = delete;
ObserverHandle& operator=(const ObserverHandle&) = delete;
};
注意:list_ 是原始指针,因为句柄不拥有容器;it_ 必须在构造时立即捕获,不能延迟获取。
std::function 回调捕获方式对生命周期的影响
回调能否安全执行,取决于捕获方式是否与被观察对象/观察者对象的生命周期对齐。错误写法直接导致 use-after-free:
-
[this]{ ... }:危险!若this所在对象先于观察者容器销毁,回调触发即未定义行为 -
[ptr = shared_from_this()]{ ... }:安全,但要求被观察类继承std::enable_shared_from_this -
[w = weak_ptr_to_observer]{ ... }:适合观察者为独立对象,回调前用w.lock()判活 - 纯函数对象(无捕获)或静态函数:最轻量,但无法访问实例状态
性能提示:捕获 std::shared_ptr 会增加原子计数开销;高频事件建议用 std::weak_ptr + lock() 判空,避免强引用循环。
线程安全边界在哪?别指望 RAII 句柄自动解决并发问题
ObserverHandle 的析构是线程安全的(只操作自身持有的迭代器和原始指针),但**观察者容器的读写本身不是线程安全的**。常见误区是以为“RAII 就等于线程安全”。
使用场景决定加锁策略:
- 单线程环境:无需锁,
std::list配合句柄完全够用 - 多生产者/单消费者(如主线程发通知、工作线程订阅):写容器时加
std::mutex,读通知循环可无锁(但需确保遍历中不被修改) - 高频多线程通知:考虑用
std::vector<:function>></:function>+ 原子索引快照,避免迭代器失效
真正容易被忽略的是:即使用了 RAII 句柄,若两个线程同时调用 subscribe(),仍需保护容器插入操作——句柄只管“退订”,不管“注册”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











