c++中如何实现简单的观察者模式_c++设计模式之回调机制【实例】

用std::function+std::vector可实现轻量观察者模式:注册回调(lambda/函数/绑定成员函数),通知时遍历调用,无虚函数开销;需防悬挂引用(推荐detach()或shared_ptr管理生命周期)。

用 std::function + std::vector 实现轻量观察者注册与通知

不需要引入 Boost 或 Qt,C++11 起就能用 std::functionstd::vector 搭出可用的观察者模式。核心是把回调抽象为可调用对象,避免继承和虚函数开销。

关键点在于:观察者不继承基类,被观察者不持有具体类型,只存 std::function 或带参数的变体(如 std::function)。

  • 注册时用 std::function 包装 lambda、普通函数或成员函数(需绑定 this
  • 通知时遍历 vector,直接调用 func() —— 无虚表跳转,性能接近裸函数指针
  • 注意:若注册的是类成员函数,必须用 std::bind 或 lambda 捕获 this,否则编译失败
#include 
#include 

class Subject {
private:
    std::vector> observers;

public:
    void attach(std::function obs) {
        observers.push_back(obs);
    }

    void notify(int value) {
        for (auto& obs : observers) {
            obs(value); // 直接调用,无多态开销
        }
    }
};

// 使用示例
int main() {
    Subject sub;
    int count = 0;

    // lambda 观察者
    sub.attach([&count](int v) { count += v; });

    // 普通函数观察者
    auto log = [](int v) { printf("log: %d\n", v); };
    sub.attach(log);

    sub.notify(42); // 触发两个回调
}

处理成员函数回调时如何避免悬挂引用

最容易踩的坑:用 [this] 捕获成员函数到 std::function,但被观察者生命周期长于观察者对象 —— this 变成悬垂指针,调用时崩溃。

这不是语法问题,而是所有权语义缺失导致的运行时错误。标准库不检查 std::function 内部是否还有效。

  • 方案一:用 std::shared_ptr 管理观察者对象,lambda 中捕获 shared_from_this()
  • 方案二:提供 detach() 接口,由观察者在析构前显式注销
  • 方案三:改用信号槽库(如 libsigc++),自带连接生命周期管理

纯标准库方案中,detach() 最轻量,但依赖人工配对调用,容易遗漏。

std::function 的性能代价与替代选项

std::function 有小对象优化(SBO),但一旦捕获大对象或使用堆分配(如绑定多个参数的 std::bind),就会触发内存分配 —— 在高频通知场景下可能成为瓶颈。

  • 若观察者数量固定且极少(≤3),可考虑模板参数化:用 std::tuple 存不同类型的回调,编译期展开
  • 若只允许函数指针(无状态),直接用 void(*)() 类型,零开销
  • Qt 的 QObject::connect 走的是元对象系统,支持跨线程队列,但重量级;这里讨论的是无框架的轻量实现

多数业务逻辑通知频率不高,std::function 的开销可忽略;真卡在这里,说明设计上可能已混淆了“事件”和“热路径计算”。

为什么不用虚函数实现经典观察者模式

教科书常写 Observer 抽象基类 + update() 虚函数,但 C++ 中这会强制继承、引入虚表、破坏内联机会,且每个观察者都要写一个类。

  • 虚函数方案适合需要运行时动态增删多种异构行为的场景(如 GUI 控件系统)
  • 但多数业务中,观察者只是“收到数据后做点事”,用 std::function 更直白、更易测试、更少样板代码
  • 虚函数还带来对象切片风险:若把派生类对象传给接受基类引用的 attach(),会丢失派生部分

现代 C++ 倾向用组合代替继承,用类型擦除(std::function)代替运行时多态 —— 不是回避设计模式,而是选更贴合语言特性的表达方式。