在现代 C++(C++17 及以后)中,std::variant 为我们提供了一种简单且类型安全的方式来存储多种类型的值。它可以用来实现一个事件系统,其中每种事件都有自己的数据结构,而不必依赖传统的继承与虚函数。本文将演示如何利用 std::variant、std::visit 和自定义事件类型来构建一个轻量级、可扩展的事件系统。
1. 设计事件类型
首先,定义几种不同的事件结构。每种事件都有自己的属性,以满足业务需求。
struct MouseEvent {
int x;
int y;
int button; // 0: left, 1: right
};
struct KeyboardEvent {
char key;
bool ctrl;
bool shift;
};
struct TimerEvent {
std::chrono::steady_clock::time_point timestamp;
};
2. 定义事件别名
利用 std::variant 将所有事件类型组合在一起:
using Event = std::variant<MouseEvent, KeyboardEvent, TimerEvent>;
此时,Event 就可以容纳任意一种事件类型,并保持类型安全。
3. 事件回调类型
我们需要一个统一的回调类型来处理所有事件。使用 std::function<void(const Event&)> 作为事件处理器:
using EventHandler = std::function<void(const Event&)>;
4. 事件总线
实现一个简单的事件总线,用于注册、触发事件。
class EventBus {
public:
// 注册回调
void subscribe(const EventHandler& handler) {
handlers_.push_back(handler);
}
// 触发事件
void publish(const Event& event) const {
for (const auto& h : handlers_) {
h(event);
}
}
private:
std::vector <EventHandler> handlers_;
};
5. 处理事件的示例
下面演示如何使用 std::visit 来根据事件类型执行不同的逻辑。
void handleEvent(const Event& e) {
std::visit([](auto&& arg){
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, MouseEvent>) {
std::cout << "Mouse at (" << arg.x << ", " << arg.y << ") button " << arg.button << '\n';
} else if constexpr (std::is_same_v<T, KeyboardEvent>) {
std::cout << "Key '" << arg.key << "' ctrl=" << arg.ctrl << " shift=" << arg.shift << '\n';
} else if constexpr (std::is_same_v<T, TimerEvent>) {
std::cout << "Timer event at " << std::chrono::duration_cast<std::chrono::milliseconds>(arg.timestamp.time_since_epoch()).count() << " ms\n";
}
}, e);
}
6. 使用完整示例
int main() {
EventBus bus;
bus.subscribe(handleEvent);
// 产生鼠标事件
MouseEvent me{100, 200, 0};
bus.publish(me);
// 产生键盘事件
KeyboardEvent ke{'A', true, false};
bus.publish(ke);
// 产生计时器事件
TimerEvent te{std::chrono::steady_clock::now()};
bus.publish(te);
return 0;
}
7. 优点与适用场景
- 类型安全:
std::variant在编译期检查类型,避免了运行时的错误。 - 零运行时开销:与传统多态相比,
std::variant只需要存储一个固定大小的联合体,避免了虚函数表的间接寻址。 - 可扩展性:只需在
Event列表中添加新的事件类型即可,无需修改已有回调代码。 - 适用于事件驱动系统:如 GUI 事件、网络协议事件、游戏引擎内部事件等。
8. 小结
通过 std::variant 与 std::visit,我们可以在 C++17 及以后版本中轻松实现一个高效、类型安全的事件系统。相比传统的继承+虚函数方法,这种方式更简洁、性能更好,且易于维护。希望本示例能为你在项目中使用 std::variant 处理多类型事件提供帮助。