用 std::function + std::vector 可实现轻量观察者模式,核心是运行时回调抽象与安全生命周期管理,避免虚函数耦合和悬挂调用。

用 std::function + std::vector 实现轻量观察者
不用第三方库、不依赖 Qt 或 Boost,纯标准 C++ 就能搭出可用的观察者链。核心是把回调抽象成 std::function<void></void>(或带参数的变体),用 std::vector 存储,触发时遍历调用。
常见错误是直接存裸函数指针或 lambda 捕获局部变量,导致调用时崩溃。必须确保所有注册的回调生命周期长于被观察对象,或者用 std::shared_ptr 管理观察者对象。
- 推荐签名:
using Callback = std::function<void event>;</void>,Event是自定义结构体,比 void* 安全、比模板泛型易维护 - 注册时别写
obs.register([]{ /* 用局部变量 */ });—— 捕获栈变量的 lambda 一出作用域就失效 - 移除回调不能只靠 erase 迭代器,得配套实现 ID 或比较逻辑,否则无法安全解绑
为什么不用虚函数多态实现 Observer 接口
传统教科书写法是定义 Observer 抽象基类,让具体类继承并重写 update()。这在 C++ 里容易引发两个实际问题:内存布局耦合和销毁顺序风险。
当被观察者持有 std::vector<:unique_ptr>></:unique_ptr>,而某个 Observer 子类对象自身又持有被观察者引用(比如 UI 控件监听数据模型),就极易形成循环引用或析构时访问已释放内存。用 std::function 则天然解耦类型,回调绑定发生在运行时,不强制继承关系。
立即学习“C++免费学习笔记(深入)”;
- 虚函数方案要求所有观察者从同一基类派生,限制了现有类的复用(比如你不能让一个
std::thread对象直接当 observer) - 虚表指针带来微小但确定的内存与调用开销;
std::function在 clang/gcc 下对空捕获 lambda 通常内联为直接调用 - 调试时,虚函数调用栈深、符号模糊;
std::function调用点清晰,gdb 里能直接看到注册位置
std::weak_ptr 配合 std::shared_ptr 解决悬挂回调
最常被忽略的坑:UI 控件作为观察者被销毁了,但被观察者还留着它的 std::function 回调,下次通知直接 crash。解决方案不是禁止销毁,而是让回调“知道自己是否还有效”。
做法是观察者对象用 std::shared_ptr 管理,注册时传入 std::weak_ptr 包装的 lambda:
auto self = shared_from_this();
obs.register([self](const Event& e) {
if (auto ptr = self.lock()) {
ptr->handle(e);
} // 否则静默丢弃
});
这要求观察者类继承 std::enable_shared_from_this,且必须由 std::make_shared 构造。硬伤是:不能用于栈对象或全局对象——它们根本没法套 shared_ptr。
- 别试图用
std::weak_ptr包裹std::function本身——std::function不支持 weak 语义 - 如果观察者是普通类实例(非 shared_ptr 管理),只能靠手动 unregister,且必须保证 unregister 调用早于对象析构
- Qt 的
QObject::connect默认做类似 weak 检查,但那是元对象系统 baked in 的行为,标准 C++ 得自己铺路
性能敏感场景下避免 std::function 的堆分配
每次 std::function 构造都可能触发一次小内存分配(尤其捕获较多变量时),高频事件(如帧更新、音频采样)中会明显拖慢吞吐。这时候得降级到函数指针 + void* 上下文,或用 std::variant 预设几种回调形态。
更务实的做法是:先用 std::function 快速验证逻辑,再针对 hot path 优化。例如把最常用的无参回调单独抽成 std::array<:function>, 8></:function>,固定大小避免动态扩容。
- Clang/GCC 对空捕获 lambda 转
std::function通常零开销;但捕获 3 个以上变量,大概率触发堆分配 - 用
sizeof(std::function<void>) == 32</void>(常见值)可粗略判断是否溢出 small buffer optimization - 别过早优化:95% 的业务事件(配置变更、网络响应)完全不需要考虑这点,先跑通再测
真正难的从来不是怎么写完一个观察者,而是想清楚谁拥有谁、谁决定谁的生命周期、以及通知失败时要不要重试或丢弃——这些不会出现在类图里,但每一条都直接对应 core dump 或数据不一致。









