在现代C++开发中,事件驱动模型经常被用于解耦模块之间的通信。传统的实现往往依赖于基类指针或std::any,但这两种方式要么牺牲类型安全,要么导致运行时开销。C++17的std::variant提供了一个轻量级、类型安全的多态容器,正好适用于此类场景。本文将通过一个完整的示例,演示如何使用std::variant构建一个可扩展的事件系统,并展示其优点。
1. 事件类型的定义
首先我们为系统定义若干事件,每个事件都有自己的数据结构。
#include <variant>
#include <vector>
#include <functional>
#include <iostream>
#include <string>
#include <chrono>
struct PlayerScored {
int playerId;
int points;
};
struct EnemyDefeated {
int enemyId;
std::string enemyType;
};
struct TimeTick {
std::chrono::milliseconds elapsed;
};
using Event = std::variant<PlayerScored, EnemyDefeated, TimeTick>;
Event是所有可能事件类型的联合。使用std::variant可以在编译期保证只存储合法的事件类型。
2. 事件监听器与分发机制
接下来实现一个事件总线(EventBus),允许注册监听器并广播事件。
class EventBus {
public:
using Listener = std::function<void(const Event&)>;
void subscribe(Listener l) {
listeners_.push_back(std::move(l));
}
void publish(const Event& e) {
for (auto& l : listeners_) {
l(e);
}
}
private:
std::vector <Listener> listeners_;
};
监听器是一个接受const Event&的函数对象。通过 publish 方法可以一次性广播任何类型的事件。
3. 事件处理示例
我们现在编写一个简单的游戏组件,演示如何订阅并处理不同事件。
void exampleUsage() {
EventBus bus;
// 监听器1:统计分数
bus.subscribe([](const Event& e) {
std::visit(overloaded {
[](const PlayerScored& p) {
std::cout << "Player " << p.playerId << " scored " << p.points << " points.\n";
},
[](const EnemyDefeated&) {},
[](const TimeTick&) {}
}, e);
});
// 监听器2:记录敌人信息
bus.subscribe([](const Event& e) {
std::visit(overloaded {
[](const PlayerScored&) {},
[](const EnemyDefeated& e) {
std::cout << "Defeated enemy " << e.enemyId << " of type " << e.enemyType << ".\n";
},
[](const TimeTick&) {}
}, e);
});
// 监听器3:更新时间
bus.subscribe([](const Event& e) {
std::visit(overloaded {
[](const PlayerScored&) {},
[](const EnemyDefeated&) {},
[](const TimeTick& t) {
std::cout << "Time elapsed: " << t.elapsed.count() << " ms\n";
}
}, e);
});
// 事件发布
bus.publish(PlayerScored{42, 10});
bus.publish(EnemyDefeated{7, "Orc"});
bus.publish(TimeTick{std::chrono::milliseconds(16)});
}
上面代码使用了 C++20 的 std::visit 与 overloaded 组合,以实现不同类型的分支。若只使用 C++17,可自行实现一个类似 overloaded 的帮助结构。
4. 运行结果
Player 42 scored 10 points.
Defeated enemy 7 of type Orc.
Time elapsed: 16 ms
每个监听器只处理其感兴趣的事件类型,其他事件被忽略,保证了类型安全且无需额外的类型检查。
5. 与传统实现的对比
| 方案 | 类型安全 | 运行时开销 | 可扩展性 | 代码简洁度 |
|---|---|---|---|---|
| 基类指针(RTTI) | 低 | 低(虚函数表) | 需要继承层次 | 中 |
std::any |
低 | 高(类型擦除) | 需要手动 any_cast |
低 |
std::variant |
高 | 低(无类型擦除) | 通过添加新类型即可扩展 | 高 |
- 类型安全:
std::variant在编译期就确定了可能的类型,避免了运行时错误。 - 开销:
std::variant的内存布局与最常见的类型相同,访问成本与普通对象相当。 - 可扩展性:新增事件只需添加结构体并更新
using Event = std::variant<...>即可,无需改动订阅逻辑。
6. 进一步优化
- 按事件类型分发:若监听器数量庞大,可在
EventBus内部维护按事件类型划分的监听器表,减少无关监听器的调用。 - 异步事件:使用
std::async或线程池,将事件分发放入任务队列,实现解耦的异步处理。 - 事件过滤:为监听器添加过滤器(如玩家ID、敌人类型等),只接收感兴趣的子集事件。
7. 结语
C++17 的 std::variant 为事件驱动架构提供了一个既类型安全又高效的工具。通过简单的封装,即可构建可维护、可扩展的事件系统,极大提升代码质量与开发效率。希望本文示例能为你在项目中实现类似机制提供参考。