观察者设计模式解决的是对象之间一对多的通知问题。当某个对象的状态发生改变时,所有依赖于它的对象都能自动收到更新,而不需要被观察者知道具体有哪些观察者存在。这种松耦合机制在GUI事件处理、发布订阅系统、MVC架构中十分常见。C++实现观察者模式需要从抽象接口入手,利用虚函数实现多态回调。下面会从结构设计、完整代码、生命周期管理以及替代方案四个层面展开讨论。

观察者模式的核心结构与接口设计
观察者模式由两个核心角色组成:被观察者(Subject)和观察者(Observer)。被观察者负责维护观察者列表,并在自身状态变化时触发通知;观察者则实现一个统一的更新接口,以便被观察者能够以多态方式调用。在C++中,观察者通常被定义为一个抽象基类,其中包含一个纯虚函数用于接收更新通知。这个纯虚函数一般命名为update,参数可以携带状态信息,例如字符串消息或者更复杂的自定义数据结构。
被观察者内部使用容器来保存观察者指针,最常用的是std::vector,因为它支持高效的顺序遍历。除此之外,还需要提供注册和注销观察者的方法。注册操作将观察者指针添加到容器末尾,注销操作则通过查找并移除对应指针来完成。通知方法的核心逻辑是遍历容器中的每个观察者,并依次调用它们的update函数。由于使用了基类指针,调用会在运行时通过虚函数表派发到正确的派生类实现,这正是多态带来的解耦效果。
接口设计的另一个关键点是析构函数。抽象基类Observer应该声明虚析构函数,以确保通过基类指针删除派生类对象时能够正确调用派生类的析构函数。即使当前实现中观察者由外部管理,虚析构函数也是一个良好的习惯。被观察者的析构函数不需要特别处理观察者,因为指针所指向的对象生命周期由外部负责,但需要注意的是,如果被观察者先于观察者销毁,而观察者仍持有对被观察者的引用,就需要额外的安全保障,这部分将在后文讨论。
完整代码示例与运行说明
下面给出一个可以直接编译运行的完整示例。该示例定义了一个抽象观察者类Observer,一个被观察者类Subject,以及两个具体观察者类ConcreteObserverA和ConcreteObserverB。运行主函数会先注册两个观察者,触发第一次通知,然后注销一个观察者,再次触发通知,以验证注销操作的效果。
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
// 抽象观察者接口
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& message) = 0;
};
// 被观察者:消息主题
class Subject {
private:
std::vector<Observer*> observers;
public:
void attach(Observer* obs) {
observers.push_back(obs);
}
void detach(Observer* obs) {
observers.erase(std::remove(observers.begin(), observers.end(), obs), observers.end());
}
void notify(const std::string& msg) {
for (Observer* obs : observers) {
obs->update(msg);
}
}
};
// 具体观察者A
class ConcreteObserverA : public Observer {
public:
void update(const std::string& msg) override {
std::cout << "Observer A got: " << msg << std::endl;
}
};
// 具体观察者B
class ConcreteObserverB : public Observer {
public:
void update(const std::string& msg) override {
std::cout << "Observer B got: " << msg << std::endl;
}
};
int main() {
Subject subject;
ConcreteObserverA obsA;
ConcreteObserverB obsB;
subject.attach(&obsA);
subject.attach(&obsB);
subject.notify("First event");
subject.detach(&obsA);
subject.notify("Second event");
return 0;
}
这段代码中,Subject维护了一个std::vector<Observer*>类型的私有成员,attach方法直接将指针压入容器,detach方法使用std::remove配合erase完成删除。notify方法通过范围for循环遍历容器,对每个观察者指针调用update方法。由于Observer类中update是纯虚函数,实际执行时会动态绑定到派生类的override版本。两个具体观察者分别在自己的update实现中打印收到的消息,从而验证通知顺序和注销效果。
运行该程序,控制台会先输出两条消息,分别来自Observer A和Observer B,然后注销Observer A后,第二次通知只会输出Observer B的消息。这个简单的示例展示了观察者模式最基础的用法,但它存在一些实际工程中需要改进的地方,比如观察者指针的生命周期管理、通知过程中修改容器导致的未定义行为,以及多线程环境下的数据竞争问题。后续章节会针对这些问题给出改进方案。
观察者生命周期与线程安全处理
上述基础实现有一个明显的缺陷:Subject存储的是裸指针,如果观察者对象在注销之前被销毁,Subject中就会残留悬空指针,一旦调用notify就会访问已释放的内存,导致程序崩溃。解决这个问题的常用方式是让Subject持有std::weak_ptr,观察者则通过std::shared_ptr管理。具体做法是观察者基类继承自std::enable_shared_from_this,Subject内部使用std::vector<std::weak_ptr<Observer>>,notify时先将weak_ptr提升为shared_ptr,成功后再调用update。这样即使观察者已被销毁,提升操作也会返回空,从而安全跳过。
另一个容易忽略的问题是通知过程中观察者注销自身。如果update函数内部调用了detach方法,而detach会修改Subject的容器,那么正在执行的for循环可能因迭代器失效而崩溃。解决方法之一是通知时先复制一份观察者列表的副本,再遍历副本,这样即使原列表被修改,遍历过程也不会受影响。复制时如果使用shared_ptr,还能保证被通知的观察者在调用期间不会被销毁。当然,这种方法会增加一定的内存开销,但换来了安全性和可预测性。
多线程场景下,Subject的notify方法可能被多个线程同时调用,同时attach和detach也可能并发执行。最简单的保护方式是给Subject添加一个std::mutex成员,在attach、detach和notify内部都使用std::lock_guard锁住互斥量。这样可以保证容器访问的原子性,但也会带来性能瓶颈,尤其是当update函数执行耗时操作时,锁会被长时间持有。更精细的方案是使用读写锁,允许并发读取观察者列表,只在修改容器时独占锁。std::shared_mutex配合std::shared_lock和std::unique_lock可以满足这一需求。
下面的代码片段展示了使用weak_ptr和锁的基本改造思路,实际项目中可以根据需要选择更合适的并发结构。
class SafeSubject {
private:
std::vector<std::weak_ptr<Observer>> observers;
mutable std::mutex mtx;
public:
void attach(std::shared_ptr<Observer> obs) {
std::lock_guard<std::mutex> lock(mtx);
observers.push_back(obs);
}
void detach(std::shared_ptr<Observer> obs) {
std::lock_guard<std::mutex> lock(mtx);
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[&obs](const std::weak_ptr<Observer>& wp) {
return wp.lock() == obs;
}),
observers.end());
}
void notify(const std::string& msg) {
std::vector<std::shared_ptr<Observer>> snapshot;
{
std::lock_guard<std::mutex> lock(mtx);
for (auto& wp : observers) {
if (auto sp = wp.lock()) {
snapshot.push_back(sp);
}
}
}
for (auto& sp : snapshot) {
sp->update(msg);
}
}
};
这个改造版本中,观察者必须通过std::shared_ptr创建并传入,Subject内部存储weak_ptr以避免延长观察者生命周期。notify方法在锁内只做快照拷贝,锁外才调用update,这样避免了长时间持锁。detach使用remove_if配合lambda表达式,通过比较weak_ptr提升后的shared_ptr是否相等来删除对应项。注意,这种实现要求观察者类继承std::enable_shared_from_this或者在外部持有shared_ptr,否则无法正确构造weak_ptr。
使用std::function回调替代继承体系
继承加虚函数是实现观察者模式的经典方式,但当观察者数量较多、接口变化频繁时,为每个观察者编写派生类会显得繁琐。现代C++提供了std::function和lambda表达式,可以用回调函数的形式轻松实现观察者模式,而不必定义继承层级。被观察者无需知道观察者的类型,只需要维护一个std::vector<std::function<void(const std::string&)>>,注册时直接传入lambda或函数对象,通知时依次调用即可。
这种回调方案的优势在于灵活性和代码简洁性。你可以把任意可调用对象注册为观察者,包括全局函数、成员函数、lambda表达式以及任何重载了operator()的类实例。同时,标准库中的std::function能够安全地管理可调用对象的生命周期,不需要手动处理指针或智能指针。但需要注意的是,std::function本身不提供注销能力,通常需要配合返回一个标识符或者使用std::list配合迭代器来支持移除。一种常见的做法是让注册函数返回一个整数ID,注销时通过ID找到对应的std::function并删除。
下面是一个使用std::function实现观察者模式的简化示例,演示了如何注册两个lambda回调并触发通知。
#include <iostream>
#include <vector>
#include <functional>
#include <string>
class CallbackSubject {
private:
std::vector<std::function<void(const std::string&)>> callbacks;
public:
void subscribe(std::function<void(const std::string&)> cb) {
callbacks.push_back(cb);
}
void notify(const std::string& msg) {
for (auto& cb : callbacks) {
cb(msg);
}
}
};
int main() {
CallbackSubject sub;
sub.subscribe([](const std::string& msg) {
std::cout << "Callback 1: " << msg << std::endl;
});
sub.subscribe([](const std::string& msg) {
std::cout << "Callback 2: " << msg << std::endl;
});
sub.notify("Functional event");
return 0;
}
回调方案的缺陷在于无法遍历时安全移除,而且失去了类型层次带来的结构化描述。如果观察者行为需要复杂的状态管理,或者多个观察者需要共享某些接口方法,继承体系仍然更合适。此外,std::function的调用存在一定的间接开销,虽然现代编译器优化后通常可以接受,但在极致性能要求的场景下,直接使用虚函数或模板回调可能更高效。实际项目中应根据具体需求权衡选择。
观察者模式在C++中还有更多变体,例如使用信号槽机制(如Boost.Signals2)或事件总线框架。无论采用哪种实现,核心思想都是将事件的产生者与处理者解耦,从而提升代码的可维护性和扩展性。理解基础实现及其潜在问题,是选择合适方案的前提。