std::function + RAII 是观察者注销最稳妥的方式,因其通过 ObserverToken 的 RAII 句柄绑定生命周期,析构时自动调用弱引用安全的注销逻辑,避免悬挂调用、循环引用与手动注销遗漏。

为什么 std::function + RAII 是观察者注销最稳妥的方式
裸指针或原始函数指针注册回调,几乎必然导致悬挂调用(use-after-free)——对象已析构,但通知还在触发。RAII 的核心不是“自动管理”,而是“生命周期绑定”:观察者注册时获得一个 std::shared_ptr 或 std::unique_ptr 管理的句柄,该句柄析构即自动从被观察者列表中移除回调。这比手动调用 unregister() 可靠得多,尤其在异常路径、早期返回、多线程竞争场景下。
关键点在于:被观察者内部必须用弱引用(std::weak_ptr)持有观察者句柄,避免循环引用;而观察者自身通过 RAII 句柄(如 ObserverToken)控制注册存续期。
ObserverToken 怎么设计才真正安全
ObserverToken 不是空壳类,它必须持有一个可销毁的资源——通常是 std::shared_ptr 指向被观察者内部的注销函数对象。当 ObserverToken 析构时,触发该函数,从被观察者容器中擦除对应项。
- 不能用
std::function直接存储注销逻辑(可能捕获局部变量),应封装为轻量 functor 或 lambda 绑定到堆对象 - token 构造函数应为
explicit,防止隐式转换误传 - 支持移动语义(
ObserverToken(ObserverToken&&)),但禁止拷贝(删除ObserverToken(const ObserverToken&)) - 提供
valid()成员函数,便于调试时检查是否已被释放
示例片段:
立即学习“C++免费学习笔记(深入)”;
class ObserverToken {
std::shared_ptr<std::function<void()>> m_cleanup;
public:
explicit ObserverToken(std::shared_ptr<std::function<void()>> cleanup)
: m_cleanup(std::move(cleanup)) {}
ObserverToken(ObserverToken&& o) noexcept : m_cleanup(std::move(o.m_cleanup)) {}
ObserverToken& operator=(ObserverToken&& o) noexcept {
m_cleanup = std::move(o.m_cleanup);
return *this;
}
~ObserverToken() { if (m_cleanup) (*m_cleanup)(); }
bool valid() const { return m_cleanup != nullptr; }
};
被观察者如何避免迭代器失效和线程竞争
注册/注销与通知同时发生时,常见错误是边遍历 std::vector<:function>></:function> 边 erase,引发迭代器失效。更糟的是,多线程下未加锁直接修改容器会触发未定义行为。
- 推荐使用
std::list<:function>></:function>或std::vector<:weak_ptr>></:weak_ptr>,配合erase-remove-if习语清理过期项 - 通知阶段应先拷贝活跃回调(
std::vector<:function>> snapshot</:function>),再遍历 snapshot 调用——这样即使原容器被其他线程修改也不影响本次通知 - 注册/注销操作需加锁(如
std::shared_mutex),但通知锁粒度要小:只锁拷贝动作,不锁调用过程 - 若确定单线程环境,可用
std::deque配合原子 size_t 计数,彻底免锁
std::function 捕获什么会导致内存泄漏或崩溃
最常踩的坑是 lambda 捕获 this 而未考虑对象生命周期:
- 用
[this]捕获:若观察者对象早于被观察者析构,后续通知将访问野指针 - 用
[ptr = shared_from_this()]捕获:前提是观察者继承自std::enable_shared_from_this,且构造时已由shared_ptr管理——否则shared_from_this()抛异常 - 捕获局部变量(如
[x = local_var])安全,但注意大对象值拷贝开销;若需引用,务必确认其生命周期长于整个观察关系 - 避免在 lambda 中调用可能抛异常的函数,除非通知逻辑明确支持异常传播(多数观察者模式选择吞掉异常,防止中断主流程)
真正轻量的关键,是把“谁负责释放”这个责任清晰地交给 RAII token,而不是指望程序员记得调用注销函数。只要 token 的生命期写对了(比如作为类成员、或限定在某段作用域内),整个机制就稳了。


















