1. 观察者模式基础回顾
观察者模式是软件设计中最常用的行为型模式之一,它定义了对象间一对多的依赖关系,当一个对象状态发生改变时,所有依赖它的对象都会自动收到通知并更新。在C++中,这种模式通常由两个核心组件构成:
- Subject(主题):维护观察者列表,提供添加/删除观察者的接口,并在状态变化时通知观察者
- Observer(观察者):定义更新接口,供主题在状态变化时调用
经典实现通常采用抽象基类+继承的方式:
cpp复制class Observer {
public:
virtual ~Observer() = default;
virtual void update() = 0;
};
class Subject {
private:
std::vector<Observer*> observers_;
public:
void attach(Observer* observer) {
observers_.push_back(observer);
}
void detach(Observer* observer) {
observers_.erase(
std::remove(observers_.begin(), observers_.end(), observer),
observers_.end()
);
}
void notify() {
for (auto observer : observers_) {
observer->update();
}
}
};
这种实现虽然直观,但在现代C++项目中会面临几个典型问题:类型安全缺失(裸指针管理)、多线程环境下的竞态条件、以及继承带来的强耦合。这些痛点促使开发者不断探索更优的变体实现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 现代C++中的观察者模式变体
2.1 基于std::function的轻量级实现
C++11引入的std::function和lambda表达式为观察者模式提供了新的实现思路。我们可以摆脱继承层次,使用函数对象作为观察者:
cpp复制class Subject {
private:
std::vector<std::function<void()>> observers_;
public:
template <typename F>
void attach(F&& f) {
observers_.emplace_back(std::forward<F>(f));
}
void notify() {
for (auto& observer : observers_) {
observer();
}
}
};
// 使用示例
Subject s;
s.attach([] { std::cout << "Observer 1 notified\n"; });
s.attach([] { std::cout << "Observer 2 notified\n"; });
s.notify();
这种变体的优势在于:
- 解除了观察者与主题的继承耦合
- 支持任意可调用对象(函数指针、成员函数、lambda等)
- 更符合现代C++的编程范式
但需要注意生命周期管理——当被捕获的局部对象销毁后,再调用观察者会导致未定义行为。解决方案可以是使用std::shared_ptr管理资源,或在detach时清理无效观察者。
2.2 线程安全的观察者容器
在多线程环境中,直接操作观察者列表可能导致竞态条件。我们可以通过以下方式增强线程安全性:
cpp复制#include <mutex>
#include <shared_mutex>
class ThreadSafeSubject {
private:
mutable std::shared_mutex mtx_;
std::vector<std::function<void()>> observers_;
public:
template <typename F>
void attach(F&& f) {
std::unique_lock lock(mtx_);
observers_.emplace_back(std::forward<F>(f));
}
void notify() {
std::shared_lock lock(mtx_);
auto local_copy = observers_; // 防止迭代器失效
lock.unlock();
for (auto& observer : local_copy) {
try {
observer();
} catch (...) {
// 异常处理逻辑
}
}
}
};
关键改进点:
- 使用std::shared_mutex实现读写锁(多个线程可并发读,但写操作独占)
- 通知时创建观察者列表的副本,避免迭代过程中容器被修改
- 添加异常处理,防止某个观察者的异常中断整个通知流程
提示:在性能敏感场景中,可以考虑无锁队列或原子操作来进一步优化。但实现复杂度会显著增加,需权衡开发成本和实际收益。
3. 特定场景下的观察者变体
3.1 事件总线模式
当系统中有大量事件类型和订阅者时,经典观察者模式会导致主题类膨胀。事件总线(Event Bus)是一种扩展变体:
cpp复制class EventBus {
private:
using HandlerList = std::vector<std::function<void()>>;
std::unordered_map<std::type_index, HandlerList> handlers_;
std::mutex mtx_;
public:
template <typename Event, typename Handler>
void subscribe(Handler&& handler) {
std::lock_guard lock(mtx_);
handlers_[typeid(Event)].emplace_back(std::forward<Handler>(handler));
}
template <typename Event>
void publish(const Event& event) {
HandlerList local_copy;
{
std::lock_guard lock(mtx_);
auto it = handlers_.find(typeid(Event));
if (it != handlers_.end()) {
local_copy = it->second;
}
}
for (auto& handler : local_copy) {
handler();
}
}
};
这种架构的特点:
- 支持多种事件类型(通过模板参数区分)
- 集中管理所有事件处理器
- 发布者与订阅者完全解耦
典型应用场景包括GUI系统的事件处理、游戏引擎中的消息传递等。在实际项目中,可以进一步扩展支持事件过滤、优先级调度等高级特性。
3.2 基于信号槽的Qt风格实现
Qt框架的信号槽机制本质上是一种类型安全的观察者模式变体。我们可以用现代C++模拟类似行为:
cpp复制template <typename... Args>
class Signal {
private:
std::vector<std::function<void(Args...)>> slots_;
public:
template <typename F>
void connect(F&& f) {
slots_.emplace_back(std::forward<F>(f));
}
void emit(Args... args) {
for (auto& slot : slots_) {
slot(args...);
}
}
};
// 使用示例
Signal<int, std::string> valueChanged;
valueChanged.connect([](int x, std::string s) {
std::cout << "Value changed to " << x << " with message " << s << "\n";
});
valueChanged.emit(42, "Hello");
这种实现提供了:
- 类型安全的参数传递
- 灵活的连接方式(支持任意可调用对象)
- 清晰的意图表达(emit明确表示信号触发)
在性能优化方面,可以考虑:
- 使用连接令牌(connection token)管理断开连接
- 实现线程间信号传递(类似Qt的QueuedConnection)
- 添加连接类型(一次性连接、自动断开连接等)
4. 性能优化与内存管理
4.1 观察者列表的内存效率
当系统中有大量观察者时,std::vector的连续内存特性可能导致性能问题。可以考虑以下优化策略:
- 分块存储:将观察者列表分成固定大小的块(如每个块64个观察者),减少重新分配的开销
cpp复制struct ObserverBlock {
static constexpr size_t BlockSize = 64;
std::array<std::function<void()>, BlockSize> observers;
size_t count = 0;
};
class Subject {
private:
std::vector<std::unique_ptr<ObserverBlock>> blocks_;
// ...
};
- 内存池预分配:提前分配固定数量的观察者槽位,避免运行时动态分配
cpp复制class ObserverPool {
private:
std::vector<std::aligned_storage_t<sizeof(std::function<void()>)>> storage_;
// ...
};
- 小型对象优化:利用std::function的小型缓冲区优化(SBO),避免小lambda的堆分配
4.2 零分配通知机制
在实时系统中,内存分配可能不可接受。可以实现无分配的观察者模式:
cpp复制template <size_t MaxObservers>
class FixedSubject {
private:
std::array<std::function<void()>, MaxObservers> observers_;
size_t count_ = 0;
public:
template <typename F>
bool attach(F&& f) {
if (count_ >= MaxObservers) return false;
observers_[count_++] = std::forward<F>(f);
return true;
}
void notify() {
for (size_t i = 0; i < count_; ++i) {
observers_[i]();
}
}
};
这种实现:
- 在编译期确定最大观察者数量
- 使用栈存储避免动态分配
- 适合嵌入式系统或性能关键路径
4.3 观察者生命周期管理
观察者模式最常见的陷阱是悬挂指针问题。现代C++提供了多种解决方案:
- 弱引用模式:使用std::weak_ptr打破循环引用
cpp复制class Observer : public std::enable_shared_from_this<Observer> {
// ...
};
class Subject {
private:
std::vector<std::weak_ptr<Observer>> observers_;
// ...
};
- 自动注销模式:观察者销毁时自动从主题注销
cpp复制class ObserverHandle {
Subject* subject_;
Observer* observer_;
public:
~ObserverHandle() {
if (subject_) subject_->detach(observer_);
}
// ...
};
- 基于令牌的连接管理:每个连接返回唯一令牌,通过令牌控制生命周期
cpp复制class Connection {
bool connected_ = true;
// ...
public:
void disconnect() { connected_ = false; }
~Connection() { disconnect(); }
};
class Subject {
public:
Connection attach(std::function<void()> f);
};
在实际项目中,我倾向于结合使用std::shared_ptr和std::weak_ptr的方案,既保证安全性又不失灵活性。对于性能敏感的场景,则采用基于令牌的手动管理方式。
