直接用 std::function 存回调易致内存泄漏,因被观察对象持有捕获已析构对象的闭包,且无法自动清理;应改用 raii 的 observertoken,配合 shared_ptr/weak_ptr 和快照通知机制确保安全。

为什么直接用 std::function 存回调容易内存泄漏?
因为观察者生命周期不可控:被观察对象(subject)持有 std::function,而该函数可能捕获了某个已析构对象的指针或引用,调用时触发未定义行为。更隐蔽的问题是——你无法在被观察对象销毁前自动清理所有注册的回调。
RAII 封装的核心目的不是“看起来优雅”,而是让「订阅」和「退订」严格绑定到作用域。只要不用裸指针、不手动管理 std::function 的存取逻辑,就能避开 90% 的悬挂回调问题。
- 别把
std::function直接塞进容器长期持有,尤其当它捕获了局部变量或this - 注册返回一个 RAII 句柄(比如
ObserverToken),其析构函数自动调用退订逻辑 - 被观察类内部用
std::vector或std::list存储回调,但只存原始可调用对象(不捕获)或用std::shared_ptr+weak_ptr配合检查
如何设计线程安全的 ObserverToken?
多数轻量级实现忽略并发场景,但实际中 UI 更新、异步任务完成等回调常跨线程。关键不是锁整个回调列表,而是确保 token 析构时能安全标记对应回调为无效,且不阻塞后续通知。
推荐做法是:token 内部持有一个 std::shared_ptr<bool></bool> 标志位,注册时生成并传入回调闭包;通知时先检查该标志是否仍为 true,再执行逻辑。这样 token 析构只需置 false,无锁、无等待。
- 不要在
ObserverToken析构里加互斥锁去删容器元素——会引发迭代器失效或死锁 - 避免用
std::map或std::unordered_map按 ID 查找再删:ID 可能重复、查找开销不必要 - 若必须支持运行时手动退订(不止靠 RAII),可用
std::atomic<size_t></size_t>做递增 token 序号,配合std::vector<:optional>></:optional>实现稀疏索引
Subject::notify() 怎么避免迭代器失效和重复通知?
常见错误是在遍历回调容器的同时允许新回调注册或旧回调退订,导致 std::vector::erase() 后迭代器全失效,或漏掉刚插入的项。根本解法是「快照+复制」:每次通知前把当前有效回调拷贝到局部 std::vector<:function>></:function>,再遍历这个副本。
这个副本代价极小(移动构造 std::function 是 O(1)),且彻底隔离了通知过程与容器变更。
- 不要在循环中调用
erase()并继续用++it——erase()返回新迭代器,不接住就悬空 - 如果回调可能重入(比如通知中又调用
subscribe()),快照还能防止无限递归或容器增长失控 - 对性能敏感场景,可加一个
bool m_notifying标志,禁止嵌套通知,比快照更省内存
示例:最小可行 RAII 观察者(无锁、无虚函数、头文件仅依赖)
以下代码可直接粘贴进头文件使用,不含任何第三方依赖:
class Subject {
std::vector<:pair>, std::shared_ptr<bool>>> m_callbacks;
mutable std::shared_mutex m_mutex;
public:
class ObserverToken {
std::shared_ptr<bool> m_alive;
public:
explicit ObserverToken(std::shared_ptr<bool> alive) : m_alive(std::move(alive)) {}
ObserverToken(const ObserverToken&) = default;
ObserverToken& operator=(const ObserverToken&) = default;
~ObserverToken() = default; // 自动置 false
};
template<typename f>
ObserverToken subscribe(F&& f) {
auto alive = std::make_shared<bool>(true);
std::unique_lock lock(m_mutex);
m_callbacks.emplace_back(std::forward<f>(f), alive);
return {alive};
}
void notify() const {
std::shared_lock lock(m_mutex);
std::vector<:function>> snapshot;
snapshot.reserve(m_callbacks.size());
for (const auto& [cb, alive] : m_callbacks) {
if (*alive) snapshot.push_back(cb);
}
for (auto& cb : snapshot) cb();
}
};
</:function></f></bool></typename></bool></bool></bool></:pair>
注意:这里用 std::shared_mutex 是为了读多写少场景优化,若编译环境不支持 C++17,换成 std::mutex 即可;ObserverToken 不提供 unsubscribe() 方法,退订完全由析构驱动——这是轻量的关键,也是最容易被误以为“不够灵活”的地方。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











