raii观察者通过弱引用句柄和guard对象实现自动注销:observer持shared_ptr令牌,subject存weak_ptr并通知前lock()校验;guard在构造时attach、析构时detach_by_index;lambda捕获weak_ptr避免循环引用。

观察者注册后如何避免手动注销导致的悬空回调?
RAII 风格的核心不是“用类封装资源”,而是让生命周期绑定对象生存期。C++ 观察者最常崩在 Subject 还活着、Observer 已析构,但回调函数指针/lambda 仍挂在列表里——下一次通知直接 UB。
解决方案是:不存裸函数指针或 std::function,改用带弱引用语义的句柄。典型做法是让 Observer 持有 std::shared_ptr<void></void> 作为“存活令牌”,注册时把该 token 的 weak_ptr 存进 Subject;每次通知前先 lock(),失败则自动剔除。
-
Subject::attach()接收std::weak_ptr<void></void>和std::function,内部用std::vector<:pair>, std::function<void>>></void></:pair>存储 - 回调执行前必须检查
weak_ptr::lock()是否返回非空shared_ptr,否则跳过并标记待清理(可惰性清理,也可即时 erase) - 不要依赖
shared_ptr的析构顺序——Observer析构时 token 自动失效,无需显式调用detach()
如何用 RAII 封装 attach/detach 成对操作?
手动配对 attach()/detach() 是反 RAII 的。正确做法是返回一个轻量级 guard 对象,在其构造时完成注册,析构时自动注销。
这个 guard 不应持有 shared_ptr 到 Observer(否则延长生命周期),而应只持有 Subject* 和一个唯一 ID 或迭代器位置,确保注销时能精准定位到对应条目。
- 推荐用
size_t索引 + “交换到尾部再 pop” 方式实现 O(1) 注销,避免遍历查找 - guard 构造函数中调用
Subject::attach()并接收返回的索引,析构时调用Subject::detach_by_index(index) - 如果
Subject使用std::vector存储,需注意:元素移动后原索引失效,所以 detach 必须在 vector 元素被移动前完成(即用 swap-remove 模式)
lambda 捕获 this 导致循环引用怎么办?
这是 RAII 观察者模式里最隐蔽的内存泄漏点:Observer 对象持有一个捕获 this 的 lambda,而该 lambda 又被 Subject 持有 —— Subject → lambda → Observer 形成闭环,Observer 永远不会析构。
根本解法是避免在 lambda 中强捕获 this。改用捕获 weak_ptr<observer></observer>,并在 lambda 内部第一行做 auto self = weak_self.lock(); if (!self) return;。
- Observer 类需继承自
std::enable_shared_from_this<observer></observer> - 注册时写成
[weak_self = shared_from_this()]() { auto self = weak_self.lock(); if (!self) return; self->on_event(); } - 绝对不要写
[this]() { ... }或[self = shared_from_this()]() { ... }—— 后者虽看似安全,但一旦Subject生命周期长于Observer,self 就成了强引用锚点
std::function vs 函数指针:性能与灵活性怎么取舍?
用 std::function 支持 lambda、成员函数、functor,但带来小对象开销和虚调用成本;用裸函数指针零开销但无法捕获上下文。
RAII 观察者若追求轻量,建议分层设计:底层存储统一用 std::function,但对外提供两个接口 —— attach_free(void(*)()) 走函数指针路径(无分配、无虚调),attach(std::function) 走通用路径。实际项目中 90% 的 observer 是无状态的事件响应,优先走 free 函数分支更稳。
- free 函数分支可将回调存为
void*+void(*)(void*),完全规避std::function的 small buffer 逻辑 - 若用
std::function,务必禁用其分配器(通过自定义 allocator 或编译期断言sizeof(std::function) ),否则可能触发堆分配 - Clang/GCC 下开启
-D_GLIBCXX_USE_CXX11_ABI=1并确认std::function实现是否启用 SBO(small buffer optimization)
真正难的不是写出能跑的观察者,而是让它的注销时机精确到毫秒级匹配对象真实生命周期——尤其是跨线程、异步回调、weak_ptr lock 失败后延迟清理等边界场景,稍不注意就会变成偶发 crash。别迷信“自动”,多打日志验证 token 销毁顺序。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











