如何使用std::variant实现类型安全的事件系统?

在现代 C++(尤其是 C++17 及以后)中,std::variant 为我们提供了一种轻量级、类型安全的方式来管理多种不同类型的数据。当我们需要实现一个事件驱动系统,事件可能有多种不同的数据结构时,std::variant 可以让我们避免传统的裸指针、void* 或繁琐的继承体系。下面以一个简化的“游戏事件系统”为例,演示如何用 std::variant + std::visit 来实现事件分发、监听以及处理。


1. 事件类型定义

首先定义几种不同的事件数据结构。我们假设有以下三种事件:

  1. PlayerMoved – 玩家移动事件,携带坐标信息。
  2. EnemySpawned – 敌人生成事件,携带敌人类型与位置。
  3. 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::visitstd::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. 关键点回顾

  1. 类型安全std::variant 通过编译期类型检查,避免了裸指针和 void* 的风险。
  2. 零运行成本:在大多数实现中,variant 采用“小内存占用”技术(如 std::aligned_storage),不会产生额外的动态分配。
  3. 灵活性:可以轻松地添加或删除事件类型,只需修改 Eventvariant 定义。
  4. 高效分发:监听器使用 std::visitget_if,只需要一次多态分发即可处理所有事件。

6. 进阶话题

  • 事件过滤:如果你想让监听器只接收特定子集的事件,可以在订阅时提供一个 std::function<bool(const Event&)> 过滤器,内部 publish 之前先调用过滤器。
  • 线程安全:如果事件总线需要在多线程环境下使用,考虑使用 std::mutex 或更高级的锁自由数据结构(如 concurrent_queue)。
  • 性能测量:在高频事件场景(如游戏循环)下,使用 std::variant 的分发速度与传统的虚函数表差距不大,甚至更好,因为没有指针间接访问。
  • 序列化:当你需要将事件写入网络或文件时,可以把每个事件结构序列化为 JSON 或二进制,然后把 Eventvariant 序列化为类型标签 + 数据块。

总结
使用 std::variant 组合事件类型,让事件系统既保持了类型安全,又保持了实现的简洁。它是现代 C++ 开发中实现轻量级事件驱动架构的理想工具。祝你编码愉快!

发表评论