C++怎么实现观察者模式_C++事件机制教程【解耦】

6次阅读

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

C++怎么实现观察者模式_C++事件机制教程【解耦】

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 或数据不一致。

text=ZqhQzanResources