直接用 std::function + std::vector 注册回调会因生命周期失控导致悬垂调用或内存错误;RAII式 ObserverHandle 通过 weak_ptr 和唯一 token 实现自动反注册,确保观察者析构时安全移除自身。

为什么直接用 std::function + std::vector 注册回调会出问题
因为生命周期管理失控:观察者(比如某个临时 lambda 捕获了局部变量)可能在被通知前就销毁,而被观察者还存着它的 std::function 副本 —— 下次调用直接 std::bad_function_call 或读已释放内存。
RAII 风格的观察者模式核心不是“怎么存回调”,而是“谁负责释放、何时释放”。常见错误是把所有权全交给被观察者,结果观察者自己都活不过一次事件循环。
- 避免裸指针或原始引用存储回调目标
- 不依赖外部手动调用
unregister()(容易漏、难追踪) - 禁止在回调执行中修改观察者容器(迭代器失效)
ObserverHandle 必须是移动语义 + 弱引用感知的句柄
它不是装饰器,是 RAII 清理契约:构造时注册,析构时自动反注册。关键在于它不能强持有被观察者,否则形成循环引用;也不能完全不管被观察者生死(否则析构时访问空指针)。
典型实现用 std::weak_ptr 指向被观察者的内部管理器(如 SubjectImpl),并在析构时尝试锁定并移除自身 ID。ID 不能是裸索引(易错位),推荐用 size_t 或 uint64_t 的唯一令牌(如原子计数器生成)。
立即学习“C++免费学习笔记(深入)”;
class ObserverHandle {
std::weak_ptr<SubjectImpl> subject_;
const size_t token_;
public:
ObserverHandle(std::shared_ptr<SubjectImpl> s, size_t t)
: subject_(s), token_(t) {}
ObserverHandle(ObserverHandle&&) noexcept = default;
ObserverHandle& operator=(ObserverHandle&&) noexcept = default;
~ObserverHandle() {
if (auto s = subject_.lock()) {
s->removeObserver(token_);
}
}
};
Subject 内部必须分离「注册/注销」和「通知」两条路径
注册和注销操作修改容器,通知过程必须只读遍历,且不能允许嵌套通知导致容器变更。最简方案是:通知前拷贝当前活跃句柄列表(std::vector<size_t></size_t>),再逐个调用;更优方案是用 std::shared_ptr<const std::vector>></const> 做快照。
- 不要在
notify()中直接遍历std::map或std::unordered_map并调用 —— 插入/删除会重哈希或重平衡 - 回调函数类型建议统一为
std::function<void event></void>,避免模板膨胀 - 若支持多事件类型,用
std::variant<eventa eventb></eventa>比虚函数更轻量
如何防止同一个观察者重复注册又不牺牲性能
靠 ObserverHandle 的唯一 token_ 不够 —— 它只管生命周期,不管逻辑重复。真正要防重入,得在 SubjectImpl::attach() 里做判断:要么要求用户传入可 hash 的 key(如 std::type_identity<t>{}</t> + 地址),要么让回调对象自己提供 id() 方法。
实践中更可靠的做法是:不禁止重复注册,但让 ObserverHandle 析构时只删最后一次注册的对应项(用栈式 token 分配 + 反向查找)。或者干脆接受“重复注册是用户误用”,在 debug build 中用 assert(!exists(token_)) 捕获。
注意:用 std::set 或 std::unordered_set 存 token 做去重,会增加每次 attach() 的对数/均摊常数开销,高频事件场景慎用。
真正的难点不在语法实现,而在厘清“谁拥有谁”——Subject 拥有回调执行权,ObserverHandle 拥有注销权,而回调闭包本身的生命期必须独立于两者。稍一混淆,就是悬垂调用或静默泄漏。


















