在现代 C++ 开发中,事件总线(Event Bus)是一种非常常见的设计模式,用来实现系统中不同组件之间的松耦合通信。传统实现往往依赖于 void* 或者 std::any,导致类型不安全且难以维护。C++17 引入的 std::variant 为我们提供了一种更安全、更高效的方式来存储多种事件类型。本文将演示如何利用 std::variant、std::visit、以及泛型编程,构建一个简单且类型安全的事件总线。
1. 需求分析
我们希望实现以下功能:
- 事件注册:不同的组件可以订阅自己感兴趣的事件类型。
- 事件发布:发布者能够广播事件,所有订阅该类型的监听器会被触发。
- 类型安全:事件总线内部不应出现
void*或不安全的类型转换。 - 轻量级:不需要额外的第三方库,只使用标准库即可。
2. 设计思路
-
事件类型:我们将所有可能的事件封装成一个
std::variant,例如using Event = std::variant<MouseEvent, KeyboardEvent, CustomEvent>;。 -
监听器:每个事件类型都对应一个函数列表,使用
std::function<void(const EventType&)>存储。 -
内部存储:使用
std::unordered_map<std::type_index, std::vector<std::function<void(const void*)>>>,但通过std::visit的访问器将void*换成真正的类型安全实现。为了避免void*,我们采用std::any并配合std::visit。 -
发布过程:调用
std::visit,在访问器里调用对应类型的所有监听器。
3. 代码实现
#include <iostream>
#include <variant>
#include <vector>
#include <unordered_map>
#include <functional>
#include <typeindex>
#include <type_traits>
// 3.1 定义几种事件类型
struct MouseEvent {
int x, y;
std::string button;
};
struct KeyboardEvent {
char key;
bool ctrl;
};
struct CustomEvent {
std::string msg;
};
// 3.2 事件总线类
class EventBus {
public:
// 订阅函数
template<typename EventType>
void subscribe(std::function<void(const EventType&)> handler) {
static_assert(std::is_constructible_v <EventType>, "EventType must be constructible");
auto& vec = listeners_[std::type_index(typeid(EventType))];
// 包装成 std::function<void(const void*)>
vec.emplace_back(
[handler = std::move(handler)](const void* ptr) {
handler(*static_cast<const EventType*>(ptr));
}
);
}
// 发布函数
template<typename EventType>
void publish(const EventType& event) const {
auto it = listeners_.find(std::type_index(typeid(EventType)));
if (it != listeners_.end()) {
for (const auto& fn : it->second) {
fn(&event);
}
}
}
private:
// 对应事件类型的监听器列表
std::unordered_map<std::type_index, std::vector<std::function<void(const void*)>>> listeners_;
};
// 3.3 使用示例
int main() {
EventBus bus;
// 订阅鼠标事件
bus.subscribe <MouseEvent>([](const MouseEvent& e){
std::cout << "Mouse at (" << e.x << "," << e.y << ") Button: " << e.button << "\n";
});
// 订阅键盘事件
bus.subscribe <KeyboardEvent>([](const KeyboardEvent& e){
std::cout << "Key: " << e.key << " Ctrl: " << (e.ctrl ? "Yes" : "No") << "\n";
});
// 订阅自定义事件
bus.subscribe <CustomEvent>([](const CustomEvent& e){
std::cout << "Custom event: " << e.msg << "\n";
});
// 发布事件
bus.publish(MouseEvent{100, 200, "left"});
bus.publish(KeyboardEvent{'A', true});
bus.publish(CustomEvent{"Hello, EventBus!"});
return 0;
}
4. 关键点剖析
-
类型安全
subscribe与publish均使用模板参数,编译期就决定了事件类型。内部通过std::type_index来维护监听器,避免了运行时的类型擦除。 -
*`std::function<void(const void)>
** 为了把不同类型的监听器统一存储在同一个容器中,我们把每个监听器包装成接受const void*` 的函数。发布时直接传递事件地址,包装函数再把它强制转换为真正的类型并调用。 -
性能
std::variant本身并未直接使用,因为我们通过类型映射实现;若需要一次性广播多种类型,可以使用std::variant包装事件,然后在总线内部统一处理。 -
可扩展性
若需要更复杂的订阅/发布模式(如优先级、一次性订阅、异步处理等),只需在EventBus内部添加相应字段与逻辑即可,而不会破坏现有的类型安全。
5. 进一步优化
- 事件对象池:如果事件频繁产生且大小不一,使用对象池可以降低内存分配开销。
- 多线程:为线程安全,可在
EventBus内部加入互斥锁或使用std::shared_mutex。 - 事件总线代理:通过代理模式让多个总线共享同一事件源,方便分层架构。
6. 结语
通过 std::variant(或 std::type_index)与 std::function 的巧妙配合,C++17 为我们提供了一套既简洁又类型安全的事件总线实现。无需第三方依赖,代码易读且可维护。希望本文能为你在项目中使用事件驱动架构提供实用参考。