RAII版观察者模式的核心是确保解注册时机确定且不可遗漏,依赖ObserverGuard在析构时自动注销回调,避免std::function捕获悬空指针导致崩溃。

RAII 版观察者模式的核心不是“自动解注册”,而是“解注册时机必须确定且不可遗漏”——std::function 本身不提供生命周期钩子,所以必须靠对象析构来触发注销,否则极易悬空回调。
为什么不能直接用 std::function 存储回调而不绑定生命周期
常见错误是把 std::function<void></void> 当作通用回调容器,往里塞一个捕获了局部对象或 this 的 lambda,然后忘记在被观察者销毁前清理。结果就是:观察者对象已析构,但被观察者内部的 std::vector<:function>></:function> 还存着指向它的函数对象,调用时直接 segmentation fault。
根本原因:std::function 是值语义,它复制的是可调用体(包括捕获列表),不感知所捕获对象的生命周期。
- 若 lambda 捕获
[this],而this所指对象先于被观察者销毁 → 崩溃 - 若 lambda 捕获局部变量(如
[x](){ use(x); }),而该作用域已退出 → 行为未定义 - 没有自动机制能“感知”被观察者是否还活着,更不会主动清空回调列表
RAII 包装器的关键设计:ObserverGuard
真正实现 RAII 的不是回调本身,而是注册行为的返回值 —— 一个轻量级守卫对象,在其析构时自动从被观察者中移除对应回调。
立即学习“C++免费学习笔记(深入)”;
示例结构:
class ObserverGuard {
Subject* subj_;
size_t id_;
public:
ObserverGuard(Subject& s, size_t id) : subj_(&s), id_(id) {}
ObserverGuard(const ObserverGuard&) = delete;
ObserverGuard& operator=(const ObserverGuard&) = delete;
~ObserverGuard() { if (subj_) subj_->unregister(id_); }
};这样,只要把 ObserverGuard 声明在合适的作用域(比如类成员、栈变量),就能保证离开作用域时自动注销。
- 注册接口返回
ObserverGuard,而不是裸 ID 或 void -
Subject::register_observer()内部生成唯一id_,并保存std::function和该 ID 的映射 - 注销必须用 ID(而非函数对象比较),因为
std::function不支持相等比较 - 避免使用
shared_ptr管理回调——它会延长观察者生命周期,违背“轻量”初衷
被观察者如何安全存储和调用 std::function
存储本身简单,但调用时容易忽略并发与迭代器失效问题。如果一边通知一边有其他线程注销,vector erase 可能导致迭代器失效,进而 crash。
推荐做法:先拷贝一份活跃回调列表,再遍历调用。
void Subject::notify() {
std::vector<std::function<void()>> callbacks;
{
std::lock_guard<std::mutex> lk(mutex_);
callbacks = callbacks_; // 浅拷贝,开销小
}
for (const auto& cb : callbacks) {
if (cb) cb(); // 防空检查(虽然一般不会为空)
}
}- 不直接在加锁区内调用回调,避免锁粒度过长或死锁(比如回调里又去调
register_observer) - 用
std::vector而非std::map存回调:ID 查找不频繁,插入/遍历性能更重要 - 回调存储用
std::function<void></void>即可,不要过度泛化成模板参数——轻量版不需要支持多参数或返回值 - 如果需要传递事件数据,让回调自己捕获,或统一用
std::function<void event></void>,但注意Event必须廉价可拷贝
实际使用时最容易漏掉的一环:守卫对象的生存期
很多人写了 ObserverGuard guard = subject.register(...),却把它放在 if 分支里、或作为临时对象传参,结果守卫一出作用域就注销,观察者根本收不到通知。
典型反例:
// ❌ 错误:临时对象,注册后立即析构
subject.register_observer([]{ log("hi"); });
<p>// ❌ 错误:if 内声明,条件不满足时根本没注册;满足时也只活到 if 结束
if (cond) {
auto guard = subject.register_observer(...); // guard 析构 → 注销
}</p><p>// ✅ 正确:成员变量 or 显式控制生命周期
class MyWidget {
ObserverGuard guard<em>;
public:
MyWidget(Subject& s) : guard</em>(s.register_observer([this]{ onEvent(); })) {}
};真正难的不是写对语法,而是判断“这个观察关系应该活多久”。UI 组件监听模型?活到组件析构。一次性的状态检查?用局部守卫配合作用域控制。这些决策比实现本身更影响健壮性。


















