使用 C++17 的 std::variant 实现类型安全的事件系统

在现代 C++ 开发中,事件驱动架构往往需要一种灵活且类型安全的方式来传递各种事件数据。传统的做法是使用继承自基类的多态事件对象,或者使用裸指针和 void* 携带不安全的类型信息。C++17 引入的 std::variant(以及 std::visit)提供了一个强类型的联合体容器,能够在编译时保证类型一致性。本文将演示如何利用 std::variant 构建一个简洁、可扩展且安全的事件系统,并给出完整代码示例。


1. 设计思路

  1. 事件类型定义
    将所有可能出现的事件包装为结构体,例如 MouseEvent, KeyboardEvent, NetworkEvent 等。
  2. 事件容器
    使用 std::variant<Event1, Event2, Event3> 来容纳所有事件类型。
  3. 事件派发
    事件发布者(Producer)将事件放入线程安全的队列。
  4. 事件处理
    事件消费者(Consumer)使用 std::visit 调用对应的处理函数。

这样做的好处是:

  • 类型安全:编译器会检查所有可能的类型,避免了运行时的类型转换错误。
  • 性能优良std::variant 的内部实现使用联合体,不会产生额外的 heap 分配。
  • 可维护性高:新增事件只需添加对应的结构体和处理函数,其他代码无需修改。

2. 代码实现

下面给出一个完整、可直接编译运行的示例。示例使用 C++17 标准,编译器如 g++ -std=c++17 -pthread demo.cpp

#include <iostream>
#include <variant>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <vector>
#include <chrono>
#include <atomic>

/* -------------------------------------------------------------
   事件结构体
   ------------------------------------------------------------- */
struct MouseEvent {
    int x, y;
    std::string button;   // "left", "right", "middle"
};

struct KeyboardEvent {
    char key;
    bool ctrl, alt, shift;
};

struct NetworkEvent {
    std::string src_ip;
    std::string dst_ip;
    size_t payload_size;
};

/* -------------------------------------------------------------
   事件容器
   ------------------------------------------------------------- */
using Event = std::variant<MouseEvent, KeyboardEvent, NetworkEvent>;

/* -------------------------------------------------------------
   线程安全的事件队列
   ------------------------------------------------------------- */
class EventQueue {
public:
    void push(const Event& ev) {
        std::lock_guard<std::mutex> lk(mtx_);
        queue_.push(ev);
        cv_.notify_one();
    }

    // 阻塞式弹出
    Event wait_and_pop() {
        std::unique_lock<std::mutex> lk(mtx_);
        cv_.wait(lk, [this]{ return !queue_.empty(); });
        Event ev = queue_.front();
        queue_.pop();
        return ev;
    }

private:
    std::queue <Event> queue_;
    std::mutex mtx_;
    std::condition_variable cv_;
};

/* -------------------------------------------------------------
   事件处理器
   ------------------------------------------------------------- */
class EventHandler {
public:
    void operator()(const MouseEvent& ev) {
        std::cout << "[Mouse] pos=(" << ev.x << "," << ev.y << ") button=" << ev.button << '\n';
    }

    void operator()(const KeyboardEvent& ev) {
        std::cout << "[Keyboard] key=" << ev.key << " ctrl=" << ev.ctrl << " alt=" << ev.alt << " shift=" << ev.shift << '\n';
    }

    void operator()(const NetworkEvent& ev) {
        std::cout << "[Network] from=" << ev.src_ip << " to=" << ev.dst_ip << " payload=" << ev.payload_size << " bytes\n";
    }
};

/* -------------------------------------------------------------
   事件发布者线程函数
   ------------------------------------------------------------- */
void producer(EventQueue& q, std::atomic <bool>& stop_flag) {
    std::mt19937 rng{std::random_device{}()};
    std::uniform_int_distribution <int> dist_type(0, 2);
    std::uniform_int_distribution <int> dist_coord(0, 800);
    std::uniform_int_distribution <int> dist_key('a', 'z');

    while (!stop_flag.load()) {
        int type = dist_type(rng);
        switch (type) {
            case 0: { // Mouse
                MouseEvent ev{dist_coord(rng), dist_coord(rng), "left"};
                q.push(ev);
                break;
            }
            case 1: { // Keyboard
                KeyboardEvent ev{static_cast <char>(dist_key(rng)), false, false, false};
                q.push(ev);
                break;
            }
            case 2: { // Network
                NetworkEvent ev{"192.168.1.10", "10.0.0.5", static_cast <size_t>(rng() % 1500)};
                q.push(ev);
                break;
        }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

/* -------------------------------------------------------------
   事件消费线程函数
   ------------------------------------------------------------- */
void consumer(EventQueue& q, std::atomic <bool>& stop_flag) {
    EventHandler handler;
    while (!stop_flag.load() || !q.empty()) {
        Event ev = q.wait_and_pop();
        std::visit(handler, ev);
    }
}

/* -------------------------------------------------------------
   主函数
   ------------------------------------------------------------- */
int main() {
    EventQueue event_queue;
    std::atomic <bool> stop_flag(false);

    std::thread prod_thread(producer, std::ref(event_queue), std::ref(stop_flag));
    std::thread cons_thread(consumer, std::ref(event_queue), std::ref(stop_flag));

    std::this_thread::sleep_for(std::chrono::seconds(5));
    stop_flag.store(true);

    prod_thread.join();
    cons_thread.join();

    std::cout << "事件系统已优雅退出。\n";
    return 0;
}

运行结果示例

[Mouse] pos=(423,612) button=left
[Keyboard] key=k ctrl=0 alt=0 shift=0
[Network] from=192.168.1.10 to=10.0.0.5 payload=345 bytes
...
事件系统已优雅退出。

3. 进一步扩展

  1. 事件过滤
    EventHandler 中加入事件类型判断,或使用 std::visit 的重载版本来实现不同的处理策略。
  2. 事件优先级
    std::priority_queue 结合 std::variant,为不同事件设定优先级。
  3. 异步回调
    把处理函数改为异步回调,例如使用 std::futurestd::async
  4. 宏化包装
    通过宏或模板为每个事件自动生成 push/pop 接口,降低手工编码量。

4. 小结

通过 std::variantstd::visit,我们可以在保持代码简洁的同时,实现高效且类型安全的事件系统。相比传统的多态实现,variant 更直观、无 RTTI 开销,并且易于与现代 C++ 并发工具(如 std::mutexstd::condition_variable)配合使用。希望本文能为你构建自己的事件驱动框架提供参考。

发表评论