在现代 C++ 开发中,事件驱动架构已经成为许多框架和库的核心。传统的事件系统往往使用基类指针或字符串标识来区分事件类型,这些做法会导致类型安全问题、性能损失以及维护成本高昂。C++20 的 std::variant(以及相关的 std::visit)为我们提供了一种优雅且高效的方式来实现类型安全的事件系统。
下面将演示如何利用 std::variant 构建一个简易的事件总线(EventBus),支持任意类型的事件,并确保编译期类型检查。
1. 设计思路
-
事件类型
事件本质是携带数据的结构体。我们将所有可能的事件定义为不同的结构体类型。 -
事件包装
std::variant用于包装所有事件类型,形成一个统一的事件类型EventVariant。 -
事件分发
订阅者(listener)通过注册回调函数,指定需要监听的事件类型。分发器在收到事件后使用std::visit调用对应的回调。 -
线程安全
为了演示核心概念,我们暂不实现完整的线程安全。但可以通过std::mutex或std::shared_mutex在实际应用中加以保障。
2. 代码实现
#include <iostream>
#include <variant>
#include <functional>
#include <vector>
#include <unordered_map>
#include <typeindex>
#include <typeinfo>
#include <memory>
// --------------------- 事件定义 ---------------------
struct UserLoginEvent {
std::string username;
std::time_t timestamp;
};
struct FileUploadedEvent {
std::string filename;
std::size_t size;
};
struct ErrorEvent {
int errorCode;
std::string message;
};
// 通过 std::variant 包装所有事件
using EventVariant = std::variant<UserLoginEvent, FileUploadedEvent, ErrorEvent>;
// --------------------- 事件总线 ---------------------
class EventBus {
public:
// 订阅回调
template <typename EventType>
void subscribe(std::function<void(const EventType&)> callback) {
// 使用 type_index 作为 map 的键
std::type_index key(typeid(EventType));
// 对于同一事件类型,存储所有回调
subscribers[key].emplace_back(
[cb = std::move(callback)](const EventVariant& ev) {
// 在访问之前保证类型正确
const EventType& specificEv = std::get <EventType>(ev);
cb(specificEv);
}
);
}
// 发布事件
void publish(const EventVariant& event) {
std::type_index key(event.index() < variantTypes.size() ?
std::type_index(typeid(variantTypes[event.index()])) :
std::type_index(typeid(void)));
auto it = subscribers.find(key);
if (it != subscribers.end()) {
// 调用所有回调
for (auto& cb : it->second) {
cb(event);
}
}
}
private:
// 存储各事件类型对应的回调列表
std::unordered_map<std::type_index, std::vector<std::function<void(const EventVariant&)>>> subscribers;
// 便于获取 variant 索引对应的 typeid
static constexpr std::array<std::type_index, 3> variantTypes = {
std::type_index(typeid(UserLoginEvent)),
std::type_index(typeid(FileUploadedEvent)),
std::type_index(typeid(ErrorEvent))
};
};
// --------------------- 使用示例 ---------------------
int main() {
EventBus bus;
// 订阅 UserLoginEvent
bus.subscribe <UserLoginEvent>([](const UserLoginEvent& ev) {
std::cout << "[Login] 用户 " << ev.username << " 在 " << std::asctime(std::localtime(&ev.timestamp)) << " 登录。\n";
});
// 订阅 FileUploadedEvent
bus.subscribe <FileUploadedEvent>([](const FileUploadedEvent& ev) {
std::cout << "[Upload] 文件 " << ev.filename << " (大小: " << ev.size << " 字节) 上传完成。\n";
});
// 订阅 ErrorEvent
bus.subscribe <ErrorEvent>([](const ErrorEvent& ev) {
std::cerr << "[Error] 代码 " << ev.errorCode << " - " << ev.message << '\n';
});
// 发布事件
bus.publish(UserLoginEvent{"alice", std::time(nullptr)});
bus.publish(FileUploadedEvent{"report.pdf", 1048576});
bus.publish(ErrorEvent{404, "资源未找到"});
return 0;
}
关键点说明
-
模板
subscribe
通过模板参数EventType,让编译器推断回调中期望的事件类型。内部使用std::type_index作为键,保证不同事件类型不会混淆。 -
(ev)` 直接获取特定事件,若类型不匹配会抛出 `std::bad_variant_access`。如果需要更通用的处理方式,可以把 `subscribe` 改为接受 `std::variant` 回调并在内部使用 `std::visit`。std::visit的替代
这里使用 `std::get -
事件分发
publish方法根据variant.index()找到对应的事件类型,并调用所有注册的回调。此实现仅演示单线程场景,若需要并发可在publish前后加锁。 -
类型安全
所有订阅与发布都在编译期绑定,错误的事件类型会在编译阶段就被捕获,极大提升可靠性。
3. 扩展思路
-
优先级调度
在subscribers的std::vector前加一个优先级字段,让高优先级订阅者先处理。 -
异步事件
将EventBus与线程池或std::async结合,支持异步回调。 -
事件过滤
允许订阅者提供过滤器函数,仅当满足条件时才触发回调。 -
多播与单播
通过std::variant的std::holds_alternative判断是否存在对应类型的订阅者。
4. 小结
利用 C++20 的 std::variant,我们能够轻松构建一个类型安全、易于维护的事件系统。相比传统的基类指针或字符串标签,variant 让事件类型在编译时得到完整检查,显著降低运行时错误。随着 C++23 的新特性不断完善,这种模式将更具可扩展性与性能优势。祝你编码愉快!