跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

C++实现轻量级观察者模式(RAII风格) _ std::function回调管理【源码】

因为std::function无唯一标识,无法精准注销;应改用含weak_ptr和唯一id的observer_handle类,支持RAII自动解绑、防循环引用及安全析构。 为什么 std::function 不能直接当观察者句柄用 因为
std::function
没有唯一标识,无法在注销时精准匹配——同一回调对象多次注册,或不同实例绑定相同 lambda,
std::find
或
erase(remove_if)
都会误删或漏删。RAII 观察者必须能“自己管自己生命周期”,关键不是存回调,而是存一个可析构、可比较、能触发反注册的句柄。 实操建议: 立即学习 “ C++免费学习笔记(深入) ”; 定义一个轻量
observer_handle
类,内部持有一个指向管理器的
weak_ptr
和唯一序列号(如
size_t id
) 注册时由管理器分配递增
id
,并把
{id, callback}
存入
std::vector
或
std::unordered_map
observer_handle
的析构函数调用管理器的私有反注册接口(需 friend),传入自己的
id
禁止拷贝,只允许移动(避免句柄被意外复制后双重析构) 如何让 observer_handle 支持栈上自动注销 RAII 的核心是“作用域即生命周期”。只要
observer_handle
是局部变量,离开作用域就自动解绑,不用手动调
unregister()
——但前提是它不能悬空:若管理器先销毁,而 handle 还活着,析构时访问已释放内存就 UB。 实操建议: 立即学习 “ C++免费学习笔记(深入) ”; 管理器(如
event_bus
)用
std::shared_ptr
管理自身生命周期,
observer_handle
持
std::weak_ptr
指向它 在
observer_handle::~observer_handle()
中先
lock()
,仅当管理器还存活才执行反注册 若管理器已销毁,handle 析构不报错、不 crash,只是静默跳过(这是合理行为) 示例片段:
observer_handle::~observer_handle() { if (auto mgr = manager_.lock()) { mgr->unregister_by_id(id_); } }
std::function 回调捕获 this 导致循环引用怎么办 这是最隐蔽的坑:如果回调是
[this]{...}
,且管理器又被
shared_ptr
持有(比如作为成员),那么
observer_handle → manager → callback → this → observer_handle
就构成闭环,整个对象永远不析构。 C知道 CSDN推出的一款AI技术问答工具 下载 实操建议: 立即学习 “ C++免费学习笔记(深入) ”; 强制要求用户用
[weak_this = weak_from_this()](){ if (auto p = weak_this.lock()) { ... } }
捕获 或者,在注册接口层面禁止接收含
this
的捕获列表(靠编译期约束难,靠文档+静态断言提示更现实) 更彻底的方案:管理器不持有
std::function
,而是持有
std::any
+ 类型擦除后的调用函数指针,完全规避捕获问题(但增加复杂度) 调试技巧:在管理器析构时加日志,若发现
callbacks_.size() > 0
,大概率就是循环引用没解掉 性能敏感场景下 vector vs unordered_map 存回调怎么选 高频触发(如每帧几十次 notify)、低频注册/注销时,用
std::vector>>
更快:连续内存 + cache 友好,遍历通知是 O(n),但现代 CPU 预取很猛;而
unordered_map
插入/删除均摊 O(1),但通知时迭代器跳转随机,实际可能更慢。 实操建议: 立即学习 “ C++免费学习笔记(深入) ”; 默认用
std::vector
,尤其适合游戏、音视频等实时性要求高的场景 若存在大量观察者但每次只通知其中几个(按条件过滤),再考虑用
unordered_map
+ id 索引加速查找 别用
std::map
:红黑树插入 O(log n),且内存碎片多,毫无优势 注意:vector 删除时用 “swap-and-pop” 而非 erase-remove(避免移动大量元素),只要保证 id 不重复即可 真正难的不是写出让回调能注册/注销的代码,而是让 handle 在任意顺序销毁(管理器先死、handle 先死、线程间传递)都不崩溃。RAII 不是语法糖,是责任边界的显式声明。

相关文章