因为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++免费学习笔记(深入)”;
- 强制要求用户用
[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<:pair std::function>>></:pair> 更快:连续内存 + 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 不重复即可


















