std::function + std::vector 不够安全,因其不管理回调对象生命周期,易在析构后调用已销毁对象导致崩溃;需用 RAII 包装器(如 ScopedConnection)配合 weak_ptr 或索引标记实现自动解绑,避免 UAF 和迭代器失效。

为什么 std::function + std::vector 不够安全?
直接用 std::vector<std::function<void()>> 存回调,看似简单,但容易在对象析构时触发已销毁对象的回调 —— 尤其当观察者是某个局部对象或被提前 delete 时。std::function 内部不持有生命期保证,调用时崩溃几乎必然发生。RAII 风格的核心不是“封装”,而是“自动解绑”:订阅即注册,离开作用域即注销,不依赖手动调用 unsubscribe()。
如何用 std::shared_ptr + weak_ptr 实现自动生命周期感知?
关键不是把回调存起来,而是把“谁有资格被通知”这个事实绑定到观察者的生命周期上。典型做法是让被观察者持有一组 std::weak_ptr<Observer>,每次通知前用 lock() 检查是否还活着。但这样要求观察者必须继承/组合一个可共享的对象 —— 更轻量的做法是:把回调包装成一个带 weak_ptr 捕获的 lambda,并在执行前检查。
实操建议:
- 定义一个管理类
CallbackManager,内部用std::vector<std::function<void()>>存回调,但每个回调都由 RAII 包装器构造 - 包装器(如
ScopedConnection)在构造时将自身弱引用注入管理器,在析构时从管理器中移除对应回调索引 - 避免使用裸指针或
this捕获;若需捕获this,必须用std::shared_from_this()并确保目标类型继承std::enable_shared_from_this - 管理器的
notify()方法应遍历回调容器,对每个std::function直接调用 —— 因为解绑已在 RAII 对象析构时完成,无需运行时检查
ScopedConnection 的实现要点与常见错误
它不是智能指针,而是一个“一次性绑定句柄”。它的析构函数必须能安全地从管理器中删除自己对应的回调,即使管理器已被销毁(此时应静默失败)。
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
常见错误现象:
- 析构时访问已销毁的
CallbackManager→ 导致 UAF(Use-After-Free) - 多个
ScopedConnection共享同一回调地址,但只删一次 → 剩余句柄再析构时报错 - 回调中又触发新的订阅/注销 → 迭代
std::vector时发生迭代器失效
规避方式:
- 管理器内部用
std::vector<std::optional<std::function<void()>>>,注销时仅置std::nullopt,notify()遍历时跳过空项;避免 erase 引起的移动 -
ScopedConnection构造时记录回调在 vector 中的索引(size_t),析构时仅对该索引位置做标记 - 不提供 public 的 copy/move 构造函数,禁用重复绑定;默认 move 是安全的,但 move 后原对象应置为无效状态(如索引设为
-1)
一个可用的最小化示例结构
以下不是完整库,而是可直接嵌入项目的骨架:
class CallbackManager {
std::vector<std::optional<std::function<void()>>> callbacks_;
public:
using Connection = size_t;
Connection subscribe(std::function<void()> cb) {
callbacks_.emplace_back(std::move(cb));
return callbacks_.size() - 1;
}
void unsubscribe(Connection c) {
if (c < callbacks_.size()) callbacks_[c].reset();
}
void notify() {
for (auto& cb : callbacks_) {
if (cb) cb();
}
}
};
<p>struct ScopedConnection {
CallbackManager<em> mgr<em> = nullptr;
CallbackManager::Connection idx</em> = -1;
ScopedConnection() = default;
ScopedConnection(CallbackManager& m, CallbackManager::Connection i)
: mgr<em>(&m), idx</em>(i) {}
~ScopedConnection() {
if (mgr<em> && idx</em> != -1) mgr<em>->unsubscribe(idx</em>);
}
ScopedConnection(ScopedConnection&& o) noexcept
: mgr<em>(o.mgr</em>), idx<em>(o.idx</em>) { o.idx<em> = -1; }
ScopedConnection& operator=(ScopedConnection&& o) noexcept {
if (this != &o) {
if (mgr</em> && idx<em> != -1) mgr</em>->unsubscribe(idx<em>);
mgr</em> = o.mgr<em>; idx</em> = o.idx<em>; o.idx</em> = -1;
}
return </em>this;
}
};</p>真正容易被忽略的是:所有涉及跨作用域传递 ScopedConnection 的场景(比如返回给调用方、存入容器),都必须确认其生命周期严格长于被观察对象;否则注销动作会滞后或失效。RAII 只管“自动”,不管“正确的时间点”。

















