在现代C++中,事件驱动编程经常被用于 GUI、游戏引擎以及网络通信等领域。传统的事件系统往往使用基类指针或 void* 来存储不同类型的事件数据,这样不仅导致类型不安全,而且需要手动管理内存。C++17 引入的 std::variant 为解决这一问题提供了极佳的工具。下面将通过一个完整的示例,演示如何利用 std::variant 构建一个简洁、类型安全且易于扩展的事件系统。
1. 事件类型定义
首先我们定义几种典型的事件数据结构。每个结构都尽量只包含与该事件相关的数据,并实现一个 toString() 方法,方便后续调试。
#include <variant>
#include <string>
#include <iostream>
#include <vector>
#include <functional>
struct MouseEvent {
int x, y;
std::string button; // "left", "right", "middle"
std::string toString() const {
return "MouseEvent(" + std::to_string(x) + "," + std::to_string(y) + "," + button + ")";
}
};
struct KeyboardEvent {
int keycode;
bool pressed;
std::string toString() const {
return "KeyboardEvent(" + std::to_string(keycode) + "," + (pressed ? "press" : "release") + ")";
}
};
struct TimerEvent {
int timerId;
std::string toString() const {
return "TimerEvent(" + std::to_string(timerId) + ")";
}
};
2. 定义事件类型别名
将所有事件类型放到一个 std::variant 中,形成统一的事件对象。这里的 Event 也可以视为“事件标签”,后面可以用 std::visit 进行类型安全的处理。
using Event = std::variant<MouseEvent, KeyboardEvent, TimerEvent>;
3. 事件监听器接口
事件监听器(Listener)需要实现一个 onEvent 接口,接受一个 Event 对象。由于 Event 可能是任意类型,监听器在内部需要对其进行访问。
class IListener {
public:
virtual void onEvent(const Event& ev) = 0;
virtual ~IListener() = default;
};
4. 事件总线(EventBus)
事件总线负责注册监听器、发送事件以及按需分发。为了实现高效分发,事件总线会为每一种事件类型维护一个单独的监听器列表。
class EventBus {
public:
using ListenerPtr = std::shared_ptr <IListener>;
// 注册监听器
void registerListener(const ListenerPtr& listener) {
listeners_.push_back(listener);
}
// 发送事件
void dispatch(const Event& ev) {
// 直接遍历所有监听器并调用 onEvent
for (auto& l : listeners_) {
l->onEvent(ev);
}
}
private:
std::vector <ListenerPtr> listeners_;
};
为什么不在 EventBus 中做类型分组?
对于小型项目,简单地遍历所有监听器足够高效;若需进一步优化,可在EventBus内部为每种事件类型维护一个单独列表,并使用std::visit只调用相应的监听器。
5. 示例监听器
下面给出两个简单的监听器:一个打印所有事件,另一个仅响应鼠标事件。
class LoggingListener : public IListener {
public:
void onEvent(const Event& ev) override {
std::visit([](auto&& e){
std::cout << "[LOG] " << e.toString() << std::endl;
}, ev);
}
};
class MouseOnlyListener : public IListener {
public:
void onEvent(const Event& ev) override {
std::visit([this](auto&& e){
using T = std::decay_t<decltype(e)>;
if constexpr (std::is_same_v<T, MouseEvent>) {
std::cout << "[MOUSE] " << e.toString() << std::endl;
}
}, ev);
}
};
说明
std::visit与 Lambda 的结合使得代码既简洁又类型安全。if constexpr在编译期判断类型,从而避免运行时开销。
6. 整体运行示例
int main() {
EventBus bus;
bus.registerListener(std::make_shared <LoggingListener>());
bus.registerListener(std::make_shared <MouseOnlyListener>());
// 模拟事件
MouseEvent me{100, 200, "left"};
KeyboardEvent ke{42, true};
TimerEvent te{7};
bus.dispatch(me);
bus.dispatch(ke);
bus.dispatch(te);
return 0;
}
输出示例
[LOG] MouseEvent(100,200,left)
[MOUSE] MouseEvent(100,200,left)
[LOG] KeyboardEvent(42,press)
[LOG] TimerEvent(7)
7. 扩展与改进
- 类型安全过滤:在
EventBus中为每种事件类型单独维护监听器列表,并使用std::visit仅通知相应列表。 - 事件优先级:为
Event结构包装一个优先级字段,或在EventBus里按优先级排序后再分发。 - 异步分发:将事件放入线程安全队列,后台线程消费并分发,实现事件总线的异步化。
- 绑定特定监听器:使用
std::function直接注册回调,而非完整的监听器对象,减少类层级。
8. 小结
利用 C++17 的 std::variant 可以在不牺牲类型安全的前提下,轻松实现多类型事件的统一包装与分发。相较于传统的基类指针或 void* 方案,std::variant 更加现代、易维护,并且在编译时即可发现类型错误。通过结合 std::visit 与模板元编程,事件系统既灵活又高效。希望本文能帮助你在项目中快速搭建起可靠的事件驱动框架。