如何实现基于RAII的轻量级观察者模式,并利用std::function进行回调管理?
- 内容介绍
- 文章标签
- 相关推荐
本文共计968个文字,预计阅读时间需要4分钟。
手动管理观察者生命周期容 易导致空悬回调,而本 身不具备被调用的对象的所有权,必须配合RAII自动清理。关键在于:
常见错误现象:std::function 捕获了 this 后,被观察对象已销毁,但观察者列表里仍存活着一个调用会 crash 的函数对象。
- 必须用
std::shared_ptr或std::weak_ptr管理被观察对象生命周期(若观察者需访问其状态) - 句柄类型不能是裸指针或引用;推荐用轻量级结构体封装
std::list<...>::iterator或原子计数器 ID - 避免在回调执行期间修改观察者容器(如边遍历边 erase),否则迭代器失效
如何设计可自动注销的订阅句柄(RAII 核心)
句柄本质是一个作用域绑定的“退订令牌”。典型做法是让 subscribe() 返回一个 ObserverHandle 对象,其析构函数调用内部 unsubscribe() 逻辑。
示例关键结构:
立即学习“C++免费学习笔记(深入)”;
struct ObserverHandle { std::list<std::function<void()>>* list_ = nullptr; std::list<std::function<void()>>::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<std::function<...>>+ 原子索引快照,避免迭代器失效
真正容易被忽略的是:即使用了 RAII 句柄,若两个线程同时调用 subscribe(),仍需保护容器插入操作——句柄只管“退订”,不管“注册”。
本文共计968个文字,预计阅读时间需要4分钟。
手动管理观察者生命周期容 易导致空悬回调,而本 身不具备被调用的对象的所有权,必须配合RAII自动清理。关键在于:
常见错误现象:std::function 捕获了 this 后,被观察对象已销毁,但观察者列表里仍存活着一个调用会 crash 的函数对象。
- 必须用
std::shared_ptr或std::weak_ptr管理被观察对象生命周期(若观察者需访问其状态) - 句柄类型不能是裸指针或引用;推荐用轻量级结构体封装
std::list<...>::iterator或原子计数器 ID - 避免在回调执行期间修改观察者容器(如边遍历边 erase),否则迭代器失效
如何设计可自动注销的订阅句柄(RAII 核心)
句柄本质是一个作用域绑定的“退订令牌”。典型做法是让 subscribe() 返回一个 ObserverHandle 对象,其析构函数调用内部 unsubscribe() 逻辑。
示例关键结构:
立即学习“C++免费学习笔记(深入)”;
struct ObserverHandle { std::list<std::function<void()>>* list_ = nullptr; std::list<std::function<void()>>::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<std::function<...>>+ 原子索引快照,避免迭代器失效
真正容易被忽略的是:即使用了 RAII 句柄,若两个线程同时调用 subscribe(),仍需保护容器插入操作——句柄只管“退订”,不管“注册”。

