在现代 C++(尤其是 C++17 及以后)中,std::variant 为我们提供了一种轻量级、类型安全的方式来管理多种不同类型的数据。当我们需要实现一个事件驱动系统,事件可能有多种不同的数据结构时,std::variant 可以让我们避免传统的裸指针、void* 或繁琐的继承体系。下面以一个简化的“游戏事件系统”为例,演示如何用 std::variant + std::visit 来实现事件分发、监听以及处理。
1. 事件类型定义
首先定义几种不同的事件数据结构。我们假设有以下三种事件:
PlayerMoved– 玩家移动事件,携带坐标信息。EnemySpawned– 敌人生成事件,携带敌人类型与位置。ItemPicked– 物品拾取事件,携带物品ID。
#include <variant>
#include <string>
#include <vector>
#include <functional>
#include <iostream>
struct PlayerMoved {
int x, y;
};
struct EnemySpawned {
std::string enemyType;
int x, y;
};
struct ItemPicked {
int itemId;
};
然后把它们组合成一个 std::variant:
using Event = std::variant<PlayerMoved, EnemySpawned, ItemPicked>;
这样 Event 就能持有任意一种事件类型,而不需要显式地记录类型。
2. 事件总线(EventBus)
我们实现一个非常简化的事件总线,用于注册监听器并广播事件。监听器本质上是一个 std::function<void(const Event&)>。
class EventBus {
public:
using Listener = std::function<void(const Event&)>;
void subscribe(Listener listener) {
listeners_.push_back(std::move(listener));
}
void publish(const Event& evt) const {
for (const auto& l : listeners_) {
l(evt);
}
}
private:
std::vector <Listener> listeners_;
};
为什么用
Event而不是单个事件类型?
因为所有监听器都只关心事件的存在,而不是具体类型;如果监听器只需要处理某种类型,它可以在内部使用std::visit或std::holds_alternative进行筛选。
3. 监听器实现
下面给出几种常见的监听器写法。
3.1 直接使用 std::visit
EventBus bus;
bus.subscribe([](const Event& evt) {
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, PlayerMoved>) {
std::cout << "[Move] Player moved to (" << arg.x << ", " << arg.y << ")\n";
} else if constexpr (std::is_same_v<T, EnemySpawned>) {
std::cout << "[Spawn] Enemy " << arg.enemyType << " appeared at (" << arg.x << ", " << arg.y << ")\n";
} else if constexpr (std::is_same_v<T, ItemPicked>) {
std::cout << "[Item] Player picked item id " << arg.itemId << "\n";
}
}, evt);
});
3.2 只关心某一种事件
如果你只想对 EnemySpawned 做处理,可以这样写:
bus.subscribe([](const Event& evt) {
if (auto p = std::get_if <EnemySpawned>(&evt)) {
std::cout << "[Handler] Enemy " << p->enemyType << " spawned at (" << p->x << ", " << p->y << ")\n";
}
});
**为什么不用 `std::get
`?** 直接 `get` 会在类型不匹配时抛异常,`get_if` 只返回指针,类型不匹配时返回 `nullptr`,更加安全。
3.3 组合多种处理方式
使用 std::variant 的优点之一是可以将不同类型的处理逻辑聚合在一个对象里。下面的 EventHandler 就是一个例子:
class EventHandler {
public:
void operator()(const PlayerMoved& m) {
std::cout << "Handler: Player moved (" << m.x << "," << m.y << ")\n";
}
void operator()(const EnemySpawned& e) {
std::cout << "Handler: Enemy spawned " << e.enemyType << " at (" << e.x << "," << e.y << ")\n";
}
void operator()(const ItemPicked& i) {
std::cout << "Handler: Item picked id " << i.itemId << "\n";
}
};
bus.subscribe([](const Event& evt) {
EventHandler h;
std::visit(h, evt);
});
4. 演示
int main() {
EventBus bus;
// 注册上面三种监听器
// ① 通用处理
bus.subscribe([](const Event& e){
std::visit([](auto&& arg){
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, PlayerMoved>)
std::cout << "[Visitor] Moved to (" << arg.x << "," << arg.y << ")\n";
}, e);
});
// ② 只关心 EnemySpawned
bus.subscribe([](const Event& e){
if (auto p = std::get_if <EnemySpawned>(&e))
std::cout << "[OnlyEnemy] " << p->enemyType << " at (" << p->x << "," << p->y << ")\n";
});
// ③ 组合处理
bus.subscribe([](const Event& e){
EventHandler h;
std::visit(h, e);
});
// 发送事件
bus.publish(PlayerMoved{10, 20});
bus.publish(EnemySpawned{"Goblin", 5, 7});
bus.publish(ItemPicked{42});
return 0;
}
运行结果示例:
[Visitor] Moved to (10,20)
[OnlyEnemy] Goblin at (5,7)
Handler: Player moved (10,20)
Handler: Enemy spawned Goblin at (5,7)
Handler: Item picked id 42
5. 关键点回顾
- 类型安全:
std::variant通过编译期类型检查,避免了裸指针和void*的风险。 - 零运行成本:在大多数实现中,
variant采用“小内存占用”技术(如std::aligned_storage),不会产生额外的动态分配。 - 灵活性:可以轻松地添加或删除事件类型,只需修改
Event的variant定义。 - 高效分发:监听器使用
std::visit或get_if,只需要一次多态分发即可处理所有事件。
6. 进阶话题
- 事件过滤:如果你想让监听器只接收特定子集的事件,可以在订阅时提供一个
std::function<bool(const Event&)>过滤器,内部publish之前先调用过滤器。 - 线程安全:如果事件总线需要在多线程环境下使用,考虑使用
std::mutex或更高级的锁自由数据结构(如concurrent_queue)。 - 性能测量:在高频事件场景(如游戏循环)下,使用
std::variant的分发速度与传统的虚函数表差距不大,甚至更好,因为没有指针间接访问。 - 序列化:当你需要将事件写入网络或文件时,可以把每个事件结构序列化为 JSON 或二进制,然后把
Event的variant序列化为类型标签 + 数据块。
总结:
使用std::variant组合事件类型,让事件系统既保持了类型安全,又保持了实现的简洁。它是现代 C++ 开发中实现轻量级事件驱动架构的理想工具。祝你编码愉快!