在现代 C++ 开发中,事件驱动架构常被用于解耦组件、实现插件化或处理多态消息。传统实现往往依赖基类和虚函数,导致类层次结构难以维护且缺乏编译时检查。C++17 引入的 std::variant 提供了一种无 RTTI、类型安全且轻量级的方式来存储和处理多种事件类型。下面给出完整的实现思路与示例代码,帮助你在项目中快速引入此模式。
1. 事件类型定义
首先确定需要在系统中使用的所有事件类型。每个事件可以用结构体或类来描述,最好保持无继承关系,避免多态带来的隐蔽错误。
struct PlayerMoveEvent {
int playerId;
float newX, newY;
};
struct EnemySpawnEvent {
int enemyId;
std::string type;
float spawnX, spawnY;
};
struct GameOverEvent {
bool winner; // true: 玩家赢,false: 电脑赢
};
using GameEvent = std::variant<PlayerMoveEvent,
EnemySpawnEvent,
GameOverEvent>;
2. 事件总线(EventBus)
事件总线负责事件的发布(publish)与订阅(subscribe)。由于我们使用 std::variant,订阅函数的签名需要对每种事件类型分别定义。
#include <functional>
#include <vector>
#include <unordered_map>
#include <variant>
#include <iostream>
class EventBus {
public:
// 订阅者类型,接受具体事件引用
template<typename EventT>
using Listener = std::function<void(const EventT&)>;
// 订阅
template<typename EventT>
void subscribe(Listener <EventT> listener) {
using Key = std::type_index;
listeners_[Key(typeid(EventT))].emplace_back(
[listener](const std::variant<PlayerMoveEvent,
EnemySpawnEvent,
GameOverEvent>& ev) {
if (const auto* p = std::get_if <EventT>(&ev))
listener(*p);
}
);
}
// 发布
template<typename EventT>
void publish(const EventT& ev) {
using Key = std::type_index;
auto it = listeners_.find(Key(typeid(EventT)));
if (it != listeners_.end()) {
for (const auto& wrapper : it->second) {
wrapper(ev);
}
}
}
private:
// key: 事件类型,value: 对应所有订阅者包装后的统一签名
std::unordered_map<std::type_index,
std::vector<std::function<void(const std::variant<PlayerMoveEvent,
EnemySpawnEvent,
GameOverEvent>&)>>>
listeners_;
};
说明
EventBus::subscribe使用std::function<void(const EventT&)>捕获具体事件类型。内部将其包装为接受std::variant的函数,以便统一存储。EventBus::publish直接调用std::variant里的事件对象。通过std::type_index进行类型匹配,确保仅调用对应类型的监听者。- 由于
std::variant仅在编译期确定类型,编译器会在std::get_if的返回值上做类型检查,避免类型转换错误。
3. 使用示例
int main() {
EventBus bus;
// 订阅玩家移动事件
bus.subscribe <PlayerMoveEvent>([](const PlayerMoveEvent& e) {
std::cout << "Player " << e.playerId << " moved to (" << e.newX << ", " << e.newY << ")\n";
});
// 订阅敌人生成事件
bus.subscribe <EnemySpawnEvent>([](const EnemySpawnEvent& e) {
std::cout << "Spawned enemy #" << e.enemyId << " (" << e.type << ") at (" << e.spawnX << ", " << e.spawnY << ")\n";
});
// 订阅游戏结束事件
bus.subscribe <GameOverEvent>([](const GameOverEvent& e) {
std::cout << "Game Over! Winner: " << (e.winner ? "Player" : "Computer") << "\n";
});
// 发布事件
bus.publish(PlayerMoveEvent{42, 10.0f, 20.0f});
bus.publish(EnemySpawnEvent{7, "Goblin", 5.0f, 5.0f});
bus.publish(GameOverEvent{true});
return 0;
}
运行结果:
Player 42 moved to (10, 20)
Spawned enemy #7 (Goblin) at (5, 5)
Game Over! Winner: Player
4. 优化与扩展
-
线程安全
如果在多线程环境下使用,可在EventBus内部添加std::mutex对listeners_进行保护,或者使用读写锁(std::shared_mutex)实现更高效的并发。 -
事件总线的生命周期
为避免事件在销毁后仍被调用,可使用std::weak_ptr或者std::shared_ptr管理监听者,或者提供unsubscribe接口。 -
事件优先级
如果需要优先处理某些事件,可在listeners_的值类型改为std::map<int, ListenerWrapper>,其中int为优先级。 -
事件过滤
通过为监听者包装额外的过滤函数(如 lambda 内部的条件判断),可以实现更细粒度的事件分发。
5. 与传统虚函数派生的比较
| 维度 | 虚函数派生 | std::variant |
|---|---|---|
| 类型安全 | 在运行时通过动态调度,错误难以在编译期发现 | 编译期类型检查,错误在编译阶段即可捕获 |
| 性能 | 可能产生额外的虚表访问开销 | 典型的 switch-case 形式,零开销 |
| 可扩展性 | 需要修改基类或引入抽象接口 | 只需添加新的类型,使用者不需要修改已有代码 |
| 维护成本 | 难以追踪所有派生类 | 通过 std::variant 的成员函数 visit 或 get_if 直接定位错误 |
6. 结语
利用 C++17 的 std::variant,我们可以轻松实现一个类型安全、无继承、零运行时开销的事件系统。它既保留了面向对象的事件驱动优点,又兼顾了现代 C++ 的类型安全和性能特点。希望这篇示例能帮助你在项目中快速落地,提升代码质量与维护性。