在现代C++中,事件驱动编程已经成为游戏引擎、GUI框架甚至网络协议栈中的核心模式。传统上,事件系统常依赖于基类指针、虚函数或字符串标识符来实现多态,往往伴随运行时类型检查、手动类型转换以及潜在的类型错误。随着C++17的到来,std::variant 这一类型安全的联合体为事件系统的实现提供了更简洁、更可靠的手段。
下面通过一个完整的示例,演示如何利用 std::variant 搭建一个轻量级、类型安全的事件系统,并进一步扩展到事件总线(EventBus)和事件监听器(Listener)的实现。
1. 定义事件类型
首先,我们为每种事件定义一个结构体。每个结构体仅包含与该事件相关的数据成员。
struct MouseMoveEvent {
int x;
int y;
};
struct MouseClickEvent {
int button; // 1: left, 2: right, 3: middle
bool pressed;
};
struct KeyboardEvent {
int keyCode;
bool pressed;
};
using Event = std::variant<MouseMoveEvent, MouseClickEvent, KeyboardEvent>;
Event 是一个 std::variant,可以容纳上述三种事件之一。借助 std::visit,我们可以在不需要显式 if 或 switch 的情况下访问事件的内容。
2. 事件监听器接口
为了实现松耦合的事件处理,我们定义一个基类 IEventListener,它声明了一个模板成员函数 onEvent。每个监听器只关心自己感兴趣的事件类型。
class IEventListener {
public:
virtual ~IEventListener() = default;
template <typename T>
void onEvent(const T& event) {
if constexpr (std::is_base_of_v<IEventListener, T>) {
// 这行不会被编译,保留以满足模板约束
}
else {
handle(event);
}
}
protected:
virtual void handle(const MouseMoveEvent&) {}
virtual void handle(const MouseClickEvent&) {}
virtual void handle(const KeyboardEvent&) {}
};
这里使用 if constexpr 进行编译期检查,确保 handle 只在子类中被实现。每个子类只需要重写它关心的事件类型的 handle 函数。
3. 事件总线(EventBus)
事件总线负责维护监听器列表并分发事件。它的实现保持极简。
class EventBus {
public:
void subscribe(std::shared_ptr <IEventListener> listener) {
listeners_.push_back(listener);
}
void emit(const Event& ev) {
for (auto& listener : listeners_) {
std::visit([&](auto&& e){ listener->onEvent(e); }, ev);
}
}
private:
std::vector<std::shared_ptr<IEventListener>> listeners_;
};
emit 方法遍历所有已注册的监听器,并对每个事件调用 std::visit,把具体事件类型传递给 listener->onEvent。由于 onEvent 是模板函数,编译器会根据事件类型自动调用对应的 handle。
4. 示例监听器
下面给出两个具体的监听器示例:一个绘图系统处理鼠标事件,另一个控制系统处理键盘事件。
class RenderSystem : public IEventListener {
protected:
void handle(const MouseMoveEvent& e) override {
std::cout << "RenderSystem: Mouse moved to (" << e.x << "," << e.y << ")\n";
}
void handle(const MouseClickEvent& e) override {
std::string action = e.pressed ? "pressed" : "released";
std::cout << "RenderSystem: Button " << e.button << " " << action << "\n";
}
};
class InputSystem : public IEventListener {
protected:
void handle(const KeyboardEvent& e) override {
std::string state = e.pressed ? "pressed" : "released";
std::cout << "InputSystem: Key " << e.keyCode << " " << state << "\n";
}
};
5. 主程序演示
int main() {
EventBus bus;
auto render = std::make_shared <RenderSystem>();
auto input = std::make_shared <InputSystem>();
bus.subscribe(render);
bus.subscribe(input);
// 发射一些事件
bus.emit(MouseMoveEvent{100, 200});
bus.emit(MouseClickEvent{1, true});
bus.emit(KeyboardEvent{65, true}); // 'A' key
return 0;
}
运行结果:
RenderSystem: Mouse moved to (100,200)
RenderSystem: Button 1 pressed
InputSystem: Key 65 pressed
6. 高级扩展
6.1 事件过滤
如果某个监听器只对特定事件源感兴趣,可以在 handle 函数中添加额外的过滤条件,例如检查事件坐标是否在某个区域。
6.2 异步事件
在多线程环境中,可以把 EventBus::emit 的实现改为将事件推入线程安全的队列,由后台线程消费并分发。
6.3 事件池
为减少频繁的动态分配,可以使用对象池技术缓存 Event 对象,或直接使用 `std::vector