直接用std::function存回调易致内存泄漏,因捕获的this或成员引用可能悬空;RAII封装ObserverHandle可确保构造注册、析构自动注销,并用weak_ptr防循环引用,支持移动语义且禁止拷贝。

为什么直接用 std::function 存回调容易内存泄漏
因为观察者注册后,若被观察对象生命周期更长,而观察者提前析构(比如局部对象、shared_ptr 释放),但回调里还捕获了已销毁对象的 this 或成员引用,调用时就会崩溃。单纯靠用户手动调用 unregister 极易遗漏——尤其异常路径或提前 return 时。
RAII 封装的核心目的不是“更酷”,是让注销动作和观察者生命周期严格绑定:构造即注册,析构即注销,不依赖人工干预。
- 必须用
weak_ptr管理被观察对象的生命周期,避免循环引用 - 回调存储不能裸存
std::function,得包装一层带自动注销逻辑的句柄 - 注销操作本身要线程安全——多个观察者可能并发析构,但注销列表只应被访问一次
ObserverHandle 必须支持移动语义且禁止拷贝
这个句柄是 RAII 的载体,代表“我正在观察”。如果允许拷贝,一个对象可能被多次注销;如果禁止移动,就无法放进容器或作为函数返回值,实用性归零。
典型错误写法是把 ObserverHandle 设为 shared_ptr ——这会导致注销时机不可控(引用计数不等于观察行为存在性);正确做法是内部持有一个 unique_ptr 指向注销任务,移动时转移所有权,拷贝则删除(= delete)。
立即学习“C++免费学习笔记(深入)”;
class ObserverHandle {
struct EraseTask {
std::weak_ptr<Subject> subject_;
size_t token_;
void operator()() const {
if (auto s = subject_.lock()) s->remove_observer(token_);
}
};
std::unique_ptr<EraseTask> task_;
public:
ObserverHandle(std::weak_ptr<Subject> s, size_t t)
: task_(std::make_unique<EraseTask>(EraseTask{s, t})) {}
ObserverHandle(ObserverHandle&&) = default;
ObserverHandle& operator=(ObserverHandle&&) = default;
ObserverHandle(const ObserverHandle&) = delete;
ObserverHandle& operator=(const ObserverHandle&) = delete;
~ObserverHandle() { if (task_) (*task_)(); }
};
被观察对象的回调列表怎么避免迭代时删除崩溃
常见坑是边遍历 std::vector<std::function> 边 erase,导致迭代器失效。但更隐蔽的问题是:多个线程同时触发通知 + 注销,仅加锁不够——锁粒度太大影响性能,锁太小又难保原子性。
推荐方案是用 token + 延迟清理:每次注册返回唯一 size_t token,回调列表用 std::unordered_map<size_t, std::function> 存储;注销时只标记该 token 为无效(如插入到 std::set<size_t>),通知时先查 token 是否有效再调用。这样遍历和删除完全解耦。
- token 类型别用
int,防止溢出后重复;用size_t或自增atomic<size_t> - 无效 token 集合建议用
std::atomic<bool>*数组替代全局 set,避免竞争热点 - 通知函数内不要在锁中调用用户回调——防止用户回调又去注册/注销,引发死锁
如何让 lambda 捕获的对象安全参与 RAII
关键不在 lambda 本身,而在你传给 subscribe 的那个 lambda 是否持有外部对象的强引用。例如:[this]{ ... } 是危险的,[w = weak_from_this()]{ if (auto p = w.lock()) { ... } } 才安全。
封装层无法替用户做这个判断,所以接口设计上要明确约束:所有回调必须接受一个参数(如 const Event&),且不暴露被观察对象裸指针。用户若真需要访问被观察对象,必须显式通过 weak_ptr 升级,失败则静默返回。
这也是为什么观察者模式封装不能“全自动”——RAII 解决的是生命周期绑定问题,不是悬空引用的兜底问题。后者永远需要用户对捕获方式保持清醒。
真正容易被忽略的点是:注销操作本身可能抛异常(比如用户回调里 throw),而析构函数不能抛异常。所以 ObserverHandle 的析构函数里必须用 noexcept + try/catch 吞掉注销过程中的异常,否则程序会 std::terminate。


















