在现代 C++ 开发中,事件驱动编程已经成为一种常见的设计模式。传统的实现方式往往依赖于基类指针和 RTTI(运行时类型识别),这不仅会带来不必要的运行时开销,还可能导致类型不安全。C++17 引入的 std::variant 为我们提供了一种更优雅、更安全的方式来处理多类型数据。下面我们将通过一个完整的例子,演示如何利用 std::variant 构建一个类型安全的事件系统。
1. 事件类型定义
首先,我们需要定义一些具体的事件类型,例如鼠标事件、键盘事件和窗口事件。每种事件都用一个结构体来表示,并实现一个 toString 方法方便调试。
#include <string>
#include <variant>
#include <vector>
#include <iostream>
#include <functional>
#include <algorithm>
#include <type_traits>
// 鼠标事件
struct MouseEvent {
int x, y;
std::string button;
std::string toString() const {
return "MouseEvent(" + std::to_string(x) + "," + std::to_string(y) + "," + button + ")";
}
};
// 键盘事件
struct KeyEvent {
char key;
bool repeat;
std::string toString() const {
return std::string("KeyEvent('") + key + "'," + (repeat ? "true" : "false") + ")";
}
};
// 窗口事件
struct WindowEvent {
int width, height;
std::string action; // "resize", "close", "minimize"
std::string toString() const {
return "WindowEvent(" + action + "," + std::to_string(width) + "," + std::to_string(height) + ")";
}
};
2. 事件包装
我们将所有事件统一包装成一个 std::variant,并给出一个别名 Event,方便后续使用。
using Event = std::variant<MouseEvent, KeyEvent, WindowEvent>;
3. 事件监听器
事件监听器需要能够接收 Event 并根据事件类型做出相应的处理。我们可以使用模板函数与 std::visit 的组合,自动为不同类型的事件调用对应的回调。
class EventDispatcher {
public:
// 注册监听器
template<typename Func>
void addListener(Func&& func) {
listeners.emplace_back([func=std::forward <Func>(func)](const Event& ev){
std::visit(func, ev);
});
}
// 触发事件
void dispatch(const Event& ev) const {
for (const auto& listener : listeners) {
listener(ev);
}
}
private:
std::vector<std::function<void(const Event&)>> listeners;
};
这里的 addListener 接受一个可调用对象,该对象本身可以是一个 lambda、函数指针或者函数对象。通过 std::visit,我们将 Event 解包并传给用户提供的回调。
4. 使用示例
下面给出一个完整的使用示例,演示如何注册不同类型的监听器,并触发事件。
int main() {
EventDispatcher dispatcher;
// 监听鼠标事件
dispatcher.addListener([](const MouseEvent& e){
std::cout << "Mouse handler: " << e.toString() << std::endl;
});
// 监听键盘事件
dispatcher.addListener([](const KeyEvent& e){
std::cout << "Key handler: " << e.toString() << std::endl;
});
// 监听窗口事件
dispatcher.addListener([](const WindowEvent& e){
std::cout << "Window handler: " << e.toString() << std::endl;
});
// 监听所有事件(多态)
dispatcher.addListener([](const Event& e){
std::cout << "Generic handler: ";
std::visit([](auto&& arg){ std::cout << arg.toString(); }, e);
std::cout << std::endl;
});
// 触发各种事件
dispatcher.dispatch(MouseEvent{100, 200, "left"});
dispatcher.dispatch(KeyEvent{'A', false});
dispatcher.dispatch(WindowEvent{800, 600, "resize"});
return 0;
}
5. 扩展:类型安全的事件总线
如果项目需要更复杂的事件总线(例如支持事件过滤、优先级、异步处理等),可以在 EventDispatcher 之上再封装一层。std::variant 让我们可以轻松地将不同类型的事件统一管理,同时保持编译期类型检查,避免了传统 RTTI 方式的缺陷。
5.1 事件过滤器
class FilteredDispatcher : public EventDispatcher {
public:
template<typename T>
void addFilter(std::function<bool(const T&)> pred) {
filters.emplace_back([pred](const Event& ev){
if (auto ptr = std::get_if <T>(&ev)) {
return pred(*ptr);
}
return true; // 其它类型不做过滤
});
}
void dispatch(const Event& ev) const {
for (const auto& f : filters) {
if (!f(ev)) return; // 过滤掉
}
EventDispatcher::dispatch(ev);
}
private:
std::vector<std::function<bool(const Event&)>> filters;
};
使用方式:
FilteredDispatcher fd;
fd.addFilter <MouseEvent>([](const MouseEvent& e){ return e.button == "left"; }); // 只处理左键
fd.dispatch(MouseEvent{10,20,"right"}); // 被过滤
fd.dispatch(MouseEvent{30,40,"left"}); // 正常处理
5.2 异步处理
可以在 EventDispatcher 内部使用 std::thread 或者 std::async 将事件分发到不同线程,配合 std::variant 依旧保持类型安全。
class AsyncDispatcher : public EventDispatcher {
public:
void dispatch(const Event& ev) const override {
std::async(std::launch::async, [this, ev](){ EventDispatcher::dispatch(ev); });
}
};
6. 小结
std::variant提供了一个类型安全的多类型容器,适合用于事件系统的统一包装。- 结合
std::visit与模板回调,我们能够在编译期解析事件类型,避免运行时类型检查的成本。 - 通过简单的设计模式(观察者模式 + 事件总线),我们可以扩展到过滤器、异步分发等高级功能。
这个基于 std::variant 的事件系统既简单易用,又兼顾了性能与类型安全,是现代 C++ 项目中处理事件的理想方案。