RAII风格观察者模式要求回调注册即绑定生命周期、析构即自动反注册,否则导致悬挂回调;核心是用ObserverHandle RAII句柄管理订阅,构造时注册、析构时反注册,禁止拷贝、允许移动,并配合O(1)注销的哈希表实现。

RAII 风格的观察者模式在 C++ 里不是“加个智能指针就完事”,核心在于:回调注册即绑定生命周期,析构即自动反注册——否则必然出现悬挂回调(dangling callback)。
为什么 std::function + 原生裸指针组合是危险源头
常见写法是把 std::function 存进容器,参数捕获 this 或局部对象指针。问题在于:被观察对象提前销毁后,回调仍可能被触发,operator() 调用时访问已释放内存。
典型错误现象:Segmentation fault、undefined behavior、偶发崩溃且难以复现。
根本原因:C++ 没有垃圾回收,std::function 不感知捕获对象的生命周期。
立即学习“C++免费学习笔记(深入)”;
可行解法:
- 用
std::shared_ptr管理被观察对象,并要求观察者也持有一个std::weak_ptr来安全访问 - 或更轻量——让回调句柄自身成为 RAII 对象,构造时注册,析构时反注册(即“订阅即拥有”语义)
ObserverHandle:一个只管“自己退订”的 RAII 句柄
它不负责管理被观察者,也不持有任何对象所有权,只承诺:只要它还活着,回调就有效;一旦它析构,就从被观察者内部列表中移除对应项。
关键设计点:
- 构造函数接收被观察者的引用(或指针)和回调函数,立即完成注册
- 析构函数调用被观察者的
unregister接口(需支持按句柄 ID 或迭代器快速删除) - 内部存储一个唯一标识(如
size_tID 或std::list<...>::iterator),避免线性查找 - 禁止拷贝(
= delete),允许移动(转移注销责任)
示例节选(简化版):
class ObserverHandle {
Observer* obs_;
size_t id_;
public:
ObserverHandle(Observer& o, std::function<void()> cb)
: obs_(&o), id_(o.register_callback(std::move(cb))) {}
ObserverHandle(ObserverHandle&& other) noexcept
: obs_(other.obs_), id_(other.id_) {
other.obs_ = nullptr;
other.id_ = 0;
}
ObserverHandle& operator=(ObserverHandle&& other) noexcept {
if (this != &other) {
reset();
obs_ = other.obs_;
id_ = other.id_;
other.obs_ = nullptr;
other.id_ = 0;
}
return *this;
}
~ObserverHandle() { reset(); }
void reset() {
if (obs_ && id_) {
obs_->unregister_callback(id_);
obs_ = nullptr;
id_ = 0;
}
}
};
Observer 如何支持 O(1) 注册/注销
不能用 std::vector<std::function<void()>> —— 删除中间元素要移动后续所有项;也不能只用 std::list 而不存迭代器 —— 每次注销还得遍历找匹配回调。
推荐结构:
- 用
std::unordered_map<size_t, std::function<void()>>存回调,ID 作 key - ID 由原子计数器(
static std::atomic_size_t next_id_{1})分配,保证唯一 -
register_callback返回该 ID;unregister_callback(size_t id)直接 erase - 若需顺序触发,额外维护一个
std::vector<size_t>记录插入顺序(仅用于遍历,不用于查找)
注意:std::function 拷贝有开销,高频触发场景建议用 std::move 传入并存储;若回调极简(如仅调成员函数),可考虑模板化 + 类型擦除省去虚调用。
实际使用时最容易漏掉的两件事
第一,忘记把 ObserverHandle 绑定到作用域生命周期上——比如在函数内临时创建却没赋给变量:
observer.subscribe([]{ do_something(); }); // ❌ 句柄临时对象,构造完立刻析构 → 回调没生效就退订了
auto h = observer.subscribe([]{ do_something(); }); // ✅ 正确
第二,跨线程使用时未加锁或未声明线程安全——std::unordered_map::erase 和插入都不是无锁操作。若观察者会被多线程调用(如信号分发+UI线程响应),必须对注册表加互斥(mutable std::mutex mtx_),且在 notify 时用 std::shared_lock 或只读快照避免通知时被修改。
RAII 封装解决的是资源归属问题,不是并发问题;后者得靠明确的同步契约。


















