在现代C++中,事件驱动编程常被用于游戏引擎、GUI框架以及网络协议栈等场景。传统实现往往依赖基类指针和虚函数,容易出现类型不匹配、内存管理困难以及缺乏编译时安全性的缺陷。C++17 引入的 std::variant 与 std::visit 为构建类型安全、无运行时开销的事件系统提供了天然的工具。
1. 基本思路
-
定义事件类型
将所有可能的事件包装成独立的结构体,并在std::variant中声明所有事件类型的联合体。例如:struct MouseMoveEvent { int x, y; }; struct KeyPressEvent { int keyCode; }; struct WindowResizeEvent { int width, height; }; using Event = std::variant<MouseMoveEvent, KeyPressEvent, WindowResizeEvent>; -
事件发布者(Publisher)
事件发布者只需要把Event对象放入一个线程安全的队列即可。可以使用std::queue<std::shared_ptr<Event>>搭配std::mutex或者更高效的concurrent_queue。 -
事件订阅者(Subscriber)
订阅者通过注册回调函数来处理特定事件类型。为避免回调中的类型判断,可以使用模板包装器:template<typename EventT> void addListener(std::function<void(const EventT&)> handler) { auto wrapper = [handler](const Event& ev) { std::visit([&](auto&& arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, EventT>) { handler(arg); } }, ev); }; listeners.emplace_back(std::move(wrapper)); }这里
listeners是std::vector<std::function<void(const Event&)>>,所有事件类型统一用std::function存储,内部通过std::visit再根据类型调用真正的处理函数。 -
事件调度
调度器从队列中取出事件,逐个调用所有监听器的包装器。由于std::visit在编译时确定类型,运行时不需要任何动态类型判断开销。
2. 代码示例
#include <variant>
#include <functional>
#include <queue>
#include <vector>
#include <mutex>
#include <memory>
#include <iostream>
struct MouseMoveEvent { int x, y; };
struct KeyPressEvent { int keyCode; };
struct WindowResizeEvent { int width, height; };
using Event = std::variant<MouseMoveEvent, KeyPressEvent, WindowResizeEvent>;
class EventBus {
public:
// 注册监听器
template<typename EventT>
void addListener(std::function<void(const EventT&)> handler) {
std::function<void(const Event&)> wrapper = [handler](const Event& ev) {
std::visit([&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, EventT>) {
handler(arg);
}
}, ev);
};
std::lock_guard<std::mutex> lk(mtx);
listeners.emplace_back(std::move(wrapper));
}
// 发布事件
void publish(const Event& ev) {
std::lock_guard<std::mutex> lk(queueMtx);
eventQueue.emplace(std::make_shared <Event>(ev));
}
// 事件循环(单线程示例)
void process() {
while (true) {
std::shared_ptr <Event> evPtr;
{
std::lock_guard<std::mutex> lk(queueMtx);
if (eventQueue.empty()) return;
evPtr = std::move(eventQueue.front());
eventQueue.pop();
}
for (auto& listener : listeners) {
listener(*evPtr);
}
}
}
private:
std::vector<std::function<void(const Event&)>> listeners;
std::queue<std::shared_ptr<Event>> eventQueue;
std::mutex mtx; // listeners 保护
std::mutex queueMtx; // eventQueue 保护
};
int main() {
EventBus bus;
bus.addListener <MouseMoveEvent>([](const MouseMoveEvent& e) {
std::cout << "Mouse moved to (" << e.x << ", " << e.y << ")\n";
});
bus.addListener <KeyPressEvent>([](const KeyPressEvent& e) {
std::cout << "Key pressed: " << e.keyCode << '\n';
});
bus.addListener <WindowResizeEvent>([](const WindowResizeEvent& e) {
std::cout << "Window resized: " << e.width << "x" << e.height << '\n';
});
bus.publish(MouseMoveEvent{100, 200});
bus.publish(KeyPressEvent{42});
bus.publish(WindowResizeEvent{800, 600});
bus.process(); // 处理所有事件
}
3. 优势分析
| 特性 | 传统基类实现 | std::variant 方案 |
|---|---|---|
| 类型安全 | 运行时 dynamic_cast,可能返回空指针 |
编译时确定类型,无需 dynamic_cast |
| 内存管理 | 需要手动管理基类指针 | 共享指针或值传递,避免悬空 |
| 扩展性 | 添加新事件需修改基类及所有派生类 | 只需在 variant 声明中添加即可 |
| 性能 | 虚函数调用 + 可能的类型检查 | std::visit 编译期展开,几乎无运行时开销 |
| 可读性 | 代码层次深、易错 | 结构化、直观、易维护 |
4. 进阶方向
- 多线程:使用
concurrent_queue或std::atomic+ lock-free 技术,提升并发处理效率。 - 事件过滤:在监听器中加入条件判断,决定是否转发给后续监听器。
- 事件总线与模块化:将事件总线拆分为独立模块,方便单元测试与插件化系统。
- 序列化/反序列化:利用
std::visit与自定义序列化框架,将事件在网络或文件中持久化。
5. 小结
通过 std::variant 与 std::visit 的组合,C++17 让事件驱动编程更具类型安全性与可维护性。相比传统的虚函数链,新的实现既省去了运行时的类型判断,又降低了内存管理的复杂度。无论是在游戏引擎、GUI 框架,还是在高性能网络服务中,都能轻松应用这一模式,构建一个既高效又健壮的事件系统。