std::function+std::shared_ptr不适合轻量级观察者因其堆分配和原子开销;应采用栈上raii句柄,构造注册、析构注销,仅存裸指针/id,用原子标记快照调用避免竞态。

为什么 std::function + std::shared_ptr 组合不适合轻量级观察者?
因为每次注册回调都要堆分配(std::function 内部可能 new,std::shared_ptr 控制块也堆分配),且引用计数带来原子操作开销。轻量级场景下,你真正需要的是:栈上生命周期可控、无锁、零分配的绑定与解绑。
RAII 风格的核心不是“用 RAII”,而是让观察者句柄本身成为资源管理单元——构造即注册,析构即注销,且不依赖堆内存。
- 避免在回调注册时触发任何动态内存分配
- 观察者句柄必须能安全拷贝(用于传参/存储),但注销逻辑只执行一次
- 被观察对象不持有观察者所有权,只存裸指针或 tag;注销靠观察者自己“反向通知”
observer_handle 如何实现“构造即注册、析构即注销”?
关键在把注销动作延迟到句柄析构,且保证只执行一次。不能靠被观察者维护列表并遍历清理(那会引入迭代器失效或线程竞争),而应由观察者主动登记一个可调用的注销函数。
典型做法是:被观察者暴露一个 register_observer 方法,返回一个 observer_handle;该句柄内部持有一个 std::function<void></void>,内容就是“从被观察者中移除我”的逻辑——但这个 std::function 本身不能捕获大对象,必须轻量。
- 推荐用捕获 lambda 包装一个裸函数指针 +
void*(如[obj, id]{ obj->unregister(id); }),而非捕获 this 指针加成员函数,避免隐式生成闭包对象 -
observer_handle的移动构造/赋值需置空原句柄的注销函数,防止重复调用 - 析构时直接调用
m_unregister(),无需检查是否已注销——设计上它就该是幂等的
struct observer_handle {
std::function<void> m_unregister;
observer_handle() = default;
observer_handle(std::function<void> f) : m_unregister(std::move(f)) {}
observer_handle(observer_handle&& o) noexcept : m_unregister(std::move(o.m_unregister)) { o.m_unregister = {}; }
observer_handle& operator=(observer_handle&& o) noexcept {
if (this != &o) {
m_unregister = std::move(o.m_unregister);
o.m_unregister = {};
}
return *this;
}
~observer_handle() { if (m_unregister) m_unregister(); }
};
</void></void>
被观察者如何避免存储 std::function 导致的分配?
不要存回调本身,只存唯一 ID 和注销函数指针。实际回调执行时,再通过 ID 查找对应逻辑——但查找不能是 map 或 vector 遍历,否则 O(n) 注册/注销太重。
更轻的方案是:每个观察者注册时,被观察者返回一个整数 ID(如自增计数器),同时将回调存入预分配的静态数组或环形缓冲区;注销时仅标记该槽位为可用。前提是观察者数量有预期上限,且允许少量内存预留。
- 若上限明确(如最多 16 个 UI 订阅者),用
std::array<:function>, N></:function>是完全零分配的 - 若需动态伸缩但拒绝堆分配,可用 arena allocator(如
std::pmr::vector配合栈内存池),但复杂度上升 - 绝对不要在
notify()中做虚函数调用或dynamic_cast——这会破坏内联,增加间接跳转
回调执行期间被注销,会发生什么?
这是最常踩的坑:A 在回调里调用 observer_handle 析构,触发注销;而被观察者正在遍历观察者列表,此时迭代器失效或访问已释放内存。
根本解法不是加锁,而是改变遍历模型:不维护“活跃观察者列表”,而用“快照+原子标记”。例如:
- 注册时将回调写入固定大小数组,并用
std::atomic<bool></bool>标记是否启用 -
notify()先原子读所有标记,构建临时栈上函数指针数组,再顺序调用 - 注销只改标记位,不改动数组内容,也不影响当前 notify 调用
这样既无锁、无分配,又天然规避了“边遍历边删”的竞态。代价是轻微冗余(已注销的槽位暂不复用),但符合轻量级定位。
真正难处理的是跨线程场景:如果注销和 notify 不在同一线程,std::atomic<bool></bool> 是必须的;但若确定单线程使用,连 atomic 都可省掉,直接用 bool + 注释说明线程约束。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











