在现代 C++ 开发中,经常需要在组件之间传递多种不同类型的消息或事件。传统的做法是使用继承和虚函数、或者使用 boost::any / std::any,但这两种方案都存在一定的缺陷:继承导致类型依赖性强,std::any 需要显式的类型转换,容易出现运行时错误。C++17 引入的 std::variant 为这些问题提供了天然的解决方案。本文将演示如何利用 std::variant 构建一个简洁、安全且可扩展的事件系统。
1. 事件类型定义
首先,我们定义一组可能出现的事件类型。每个事件都是一个结构体,包含必要的数据字段。
#include <variant>
#include <string>
#include <iostream>
#include <vector>
#include <functional>
#include <chrono>
#include <thread>
// 事件:键盘按键
struct KeyPressEvent {
int keycode; // 按键码
bool repeat; // 是否为重复按键
};
// 事件:窗口尺寸变化
struct ResizeEvent {
int width;
int height;
};
// 事件:鼠标点击
struct MouseClickEvent {
int x, y; // 鼠标坐标
int button; // 按钮编号
};
// 事件:自定义日志事件
struct LogEvent {
std::string message;
int level; // 日志等级
};
2. 事件别名
接下来,用 std::variant 把所有事件类型打包成一个统一的事件类型别名。这样,一个事件对象就能携带多种可能的类型之一。
using Event = std::variant<KeyPressEvent, ResizeEvent, MouseClickEvent, LogEvent>;
3. 事件总线(EventBus)实现
事件总线是负责事件发布和订阅的核心组件。下面给出一个简易实现,采用了 std::function 来保存回调,并使用 std::unordered_map 根据事件类型映射到对应的回调列表。
#include <unordered_map>
#include <typeindex>
#include <functional>
#include <mutex>
class EventBus {
public:
// 订阅指定事件类型的回调
template<typename T>
void subscribe(std::function<void(const T&)> handler) {
std::lock_guard<std::mutex> lock(mutex_);
auto& vec = handlers_[std::type_index(typeid(T))];
vec.emplace_back([handler](const Event& ev) {
handler(std::get <T>(ev));
});
}
// 发布事件
void publish(const Event& ev) const {
std::lock_guard<std::mutex> lock(mutex_);
auto it = handlers_.find(std::type_index(ev.index() == 0 ? typeid(KeyPressEvent) :
ev.index() == 1 ? typeid(ResizeEvent) :
ev.index() == 2 ? typeid(MouseClickEvent) :
typeid(LogEvent)));
if (it != handlers_.end()) {
for (auto& cb : it->second) {
cb(ev);
}
}
}
private:
mutable std::mutex mutex_;
// key: 事件类型, value: 该类型的回调列表
std::unordered_map<std::type_index, std::vector<std::function<void(const Event&)>>> handlers_;
};
说明
subscribe使用模板,用户只需要提供对应类型的回调即可。内部会把它包装成统一的Event回调。publish根据事件内部类型索引(std::variant::index())找到对应的回调列表并执行。- 线程安全:通过
std::mutex保护内部容器,避免多线程并发访问导致的竞态。
4. 示例:使用 EventBus
下面给出一个完整的示例,演示如何订阅不同类型的事件以及如何发布事件。
int main() {
EventBus bus;
// 订阅键盘事件
bus.subscribe <KeyPressEvent>([](const KeyPressEvent& ev){
std::cout << "[KeyPress] keycode=" << ev.keycode << ", repeat=" << ev.repeat << '\n';
});
// 订阅窗口尺寸变化事件
bus.subscribe <ResizeEvent>([](const ResizeEvent& ev){
std::cout << "[Resize] width=" << ev.width << ", height=" << ev.height << '\n';
});
// 订阅鼠标点击事件
bus.subscribe <MouseClickEvent>([](const MouseClickEvent& ev){
std::cout << "[MouseClick] (" << ev.x << ", " << ev.y << "), button=" << ev.button << '\n';
});
// 订阅日志事件
bus.subscribe <LogEvent>([](const LogEvent& ev){
std::cout << "[Log] level=" << ev.level << ", message=\"" << ev.message << "\"\n";
});
// 发布若干事件
bus.publish(KeyPressEvent{ 65, false }); // 'A' 键
bus.publish(ResizeEvent{ 1280, 720 });
bus.publish(MouseClickEvent{ 400, 300, 1 });
bus.publish(LogEvent{ "Hello, EventBus!", 2 });
return 0;
}
5. 优点与扩展
- 类型安全:使用
std::variant和std::get能在编译时捕获类型错误。 - 灵活可扩展:只需在事件类型列表中添加新的结构体,订阅者无需改动其他代码。
- 轻量化:与传统的多态方案相比,
std::variant的实现更简洁、无虚函数表开销。 - 可组合:事件系统可以与异步任务、消息队列等结合,实现复杂的事件驱动架构。
高级技巧
- 可变参数订阅:通过
std::apply结合std::tuple,让回调可以同时接收多种事件类型。- 事件过滤:在回调中添加条件语句,实现基于事件内容的过滤。
- 异步处理:把
publish内部包装成std::async或者使用std::thread执行回调,提升并发性能。
6. 小结
本文展示了如何利用 C++17 的 std::variant 创建一个简洁、类型安全的事件系统。相较于传统继承和 std::any 的方案,它更易于维护、更少错误,并且在性能上也有优势。希望此示例能帮助你在项目中快速集成类型安全的事件总线,进一步提升代码质量与可维护性。