在现代 C++ 开发中,事件驱动模式被广泛用于 GUI、网络框架以及游戏引擎等场景。传统的实现方式往往依赖于继承和虚函数,导致代码耦合度高、类型转换繁琐。C++17 的 std::variant 提供了一种类型安全、轻量级的替代方案,下面将通过一个完整的示例,演示如何利用 std::variant 构建一个高效、易维护的事件系统。
1. 需求分析
我们需要一个事件总线(EventBus),能够:
- 注册事件监听器;
- 发送事件;
- 事件类型多样,且不需要在运行时进行显式的类型转换;
- 在编译时保证类型安全。
典型的事件类型可以是:
KeyEvent:键盘按键信息MouseEvent:鼠标点击或移动TimerEvent:定时器到期
2. 事件结构体定义
#include <string>
#include <variant>
#include <vector>
#include <functional>
#include <iostream>
#include <unordered_map>
struct KeyEvent {
int keyCode;
bool pressed;
};
struct MouseEvent {
int x, y;
int button; // 1: left, 2: right
};
struct TimerEvent {
std::string timerName;
double elapsed; // 秒
};
3. 事件总线实现
3.1 事件类型别名
我们通过 std::variant 定义所有可能的事件类型:
using Event = std::variant<KeyEvent, MouseEvent, TimerEvent>;
3.2 监听器类型
为了解耦,我们把监听器定义为接受对应事件类型的函数对象。为了在运行时能够区分不同的事件,我们使用 std::function,并将其包装在一个统一的接口中。
struct Listener {
// 用于识别监听器所关心的事件类型
std::size_t typeIndex;
// 事件回调,使用 std::any 以便统一调用
std::function<void(const Event&)> callback;
Listener(std::size_t idx, std::function<void(const Event&)> cb)
: typeIndex(idx), callback(std::move(cb)) {}
};
3.3 EventBus 类
class EventBus {
public:
// 注册事件监听器
template<typename EventT>
void subscribe(std::function<void(const EventT&)> cb) {
std::size_t idx = std::variant_alternative_index<EventT, Event>();
listeners[idx].emplace_back(idx, [cb = std::move(cb)](const Event& ev) {
cb(std::get <EventT>(ev)); // 直接从 variant 提取
});
}
// 触发事件
void publish(const Event& ev) {
std::size_t idx = ev.index();
auto it = listeners.find(idx);
if (it != listeners.end()) {
for (auto& listener : it->second) {
listener.callback(ev);
}
}
}
private:
std::unordered_map<std::size_t, std::vector<Listener>> listeners;
};
3.4 说明
std::variant_alternative_index<EventT, Event>()返回事件类型在Event中的索引(0、1、2…)。这是编译时已知的,保证了类型安全。publish根据ev.index()找到对应的监听器列表,随后逐个调用。由于 callback 内部已经做了 `std::get (ev)`,编译器会在编译时检查类型匹配。- 这样,即使事件类型数量扩大,只需在事件结构体列表中添加即可,系统不需要额外的 RTTI 或手工
dynamic_cast。
4. 示例使用
int main() {
EventBus bus;
// 注册键盘事件监听器
bus.subscribe <KeyEvent>([](const KeyEvent& ev) {
std::cout << "KeyEvent: code=" << ev.keyCode << " pressed=" << ev.pressed << '\n';
});
// 注册鼠标事件监听器
bus.subscribe <MouseEvent>([](const MouseEvent& ev) {
std::cout << "MouseEvent: (" << ev.x << "," << ev.y << ") button=" << ev.button << '\n';
});
// 注册定时器事件监听器
bus.subscribe <TimerEvent>([](const TimerEvent& ev) {
std::cout << "TimerEvent: " << ev.timerName << " elapsed=" << ev.elapsed << "s\n";
});
// 触发事件
bus.publish(KeyEvent{42, true});
bus.publish(MouseEvent{100, 200, 1});
bus.publish(TimerEvent{"heartbeat", 0.033});
return 0;
}
运行结果:
KeyEvent: code=42 pressed=1
MouseEvent: (100,200) button=1
TimerEvent: heartbeat elapsed=0.033s
5. 性能与可维护性评估
| 维度 | 传统虚函数方式 | std::variant 方式 |
|---|---|---|
| 类型安全 | 运行时类型检查(可能抛异常) | 编译时类型检查 |
| 耦合度 | 需要继承关系 | 无继承,解耦 |
| 运行时成本 | 虚函数表跳转 | 直接索引 + lambda 调用(无多态) |
| 代码可读性 | 需要多重 dynamic_cast |
直接 std::get |
| 可扩展性 | 需修改基类 | 仅追加 struct 和订阅即可 |
结论
利用 C++17 的 std::variant,我们可以构建一个既类型安全又轻量级的事件系统。相比传统的虚函数+继承实现,它减少了运行时开销,消除了 RTTI 的依赖,提升了代码的可维护性与可读性。对于需要大量事件类型、频繁触发的应用场景(如游戏引擎、实时 UI),推荐使用该方式。