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

在现代 C++ 开发中,事件驱动编程已经成为一种常见的设计模式。传统的实现方式往往依赖于基类指针和 RTTI(运行时类型识别),这不仅会带来不必要的运行时开销,还可能导致类型不安全。C++17 引入的 std::variant 为我们提供了一种更优雅、更安全的方式来处理多类型数据。下面我们将通过一个完整的例子,演示如何利用 std::variant 构建一个类型安全的事件系统。

1. 事件类型定义

首先,我们需要定义一些具体的事件类型,例如鼠标事件、键盘事件和窗口事件。每种事件都用一个结构体来表示,并实现一个 toString 方法方便调试。

#include <string>
#include <variant>
#include <vector>
#include <iostream>
#include <functional>
#include <algorithm>
#include <type_traits>

// 鼠标事件
struct MouseEvent {
    int x, y;
    std::string button;

    std::string toString() const {
        return "MouseEvent(" + std::to_string(x) + "," + std::to_string(y) + "," + button + ")";
    }
};

// 键盘事件
struct KeyEvent {
    char key;
    bool repeat;

    std::string toString() const {
        return std::string("KeyEvent('") + key + "'," + (repeat ? "true" : "false") + ")";
    }
};

// 窗口事件
struct WindowEvent {
    int width, height;
    std::string action; // "resize", "close", "minimize"

    std::string toString() const {
        return "WindowEvent(" + action + "," + std::to_string(width) + "," + std::to_string(height) + ")";
    }
};

2. 事件包装

我们将所有事件统一包装成一个 std::variant,并给出一个别名 Event,方便后续使用。

using Event = std::variant<MouseEvent, KeyEvent, WindowEvent>;

3. 事件监听器

事件监听器需要能够接收 Event 并根据事件类型做出相应的处理。我们可以使用模板函数与 std::visit 的组合,自动为不同类型的事件调用对应的回调。

class EventDispatcher {
public:
    // 注册监听器
    template<typename Func>
    void addListener(Func&& func) {
        listeners.emplace_back([func=std::forward <Func>(func)](const Event& ev){
            std::visit(func, ev);
        });
    }

    // 触发事件
    void dispatch(const Event& ev) const {
        for (const auto& listener : listeners) {
            listener(ev);
        }
    }

private:
    std::vector<std::function<void(const Event&)>> listeners;
};

这里的 addListener 接受一个可调用对象,该对象本身可以是一个 lambda、函数指针或者函数对象。通过 std::visit,我们将 Event 解包并传给用户提供的回调。

4. 使用示例

下面给出一个完整的使用示例,演示如何注册不同类型的监听器,并触发事件。

int main() {
    EventDispatcher dispatcher;

    // 监听鼠标事件
    dispatcher.addListener([](const MouseEvent& e){
        std::cout << "Mouse handler: " << e.toString() << std::endl;
    });

    // 监听键盘事件
    dispatcher.addListener([](const KeyEvent& e){
        std::cout << "Key handler: " << e.toString() << std::endl;
    });

    // 监听窗口事件
    dispatcher.addListener([](const WindowEvent& e){
        std::cout << "Window handler: " << e.toString() << std::endl;
    });

    // 监听所有事件(多态)
    dispatcher.addListener([](const Event& e){
        std::cout << "Generic handler: ";
        std::visit([](auto&& arg){ std::cout << arg.toString(); }, e);
        std::cout << std::endl;
    });

    // 触发各种事件
    dispatcher.dispatch(MouseEvent{100, 200, "left"});
    dispatcher.dispatch(KeyEvent{'A', false});
    dispatcher.dispatch(WindowEvent{800, 600, "resize"});

    return 0;
}

5. 扩展:类型安全的事件总线

如果项目需要更复杂的事件总线(例如支持事件过滤、优先级、异步处理等),可以在 EventDispatcher 之上再封装一层。std::variant 让我们可以轻松地将不同类型的事件统一管理,同时保持编译期类型检查,避免了传统 RTTI 方式的缺陷。

5.1 事件过滤器

class FilteredDispatcher : public EventDispatcher {
public:
    template<typename T>
    void addFilter(std::function<bool(const T&)> pred) {
        filters.emplace_back([pred](const Event& ev){
            if (auto ptr = std::get_if <T>(&ev)) {
                return pred(*ptr);
            }
            return true; // 其它类型不做过滤
        });
    }

    void dispatch(const Event& ev) const {
        for (const auto& f : filters) {
            if (!f(ev)) return; // 过滤掉
        }
        EventDispatcher::dispatch(ev);
    }

private:
    std::vector<std::function<bool(const Event&)>> filters;
};

使用方式:

FilteredDispatcher fd;
fd.addFilter <MouseEvent>([](const MouseEvent& e){ return e.button == "left"; }); // 只处理左键
fd.dispatch(MouseEvent{10,20,"right"}); // 被过滤
fd.dispatch(MouseEvent{30,40,"left"});  // 正常处理

5.2 异步处理

可以在 EventDispatcher 内部使用 std::thread 或者 std::async 将事件分发到不同线程,配合 std::variant 依旧保持类型安全。

class AsyncDispatcher : public EventDispatcher {
public:
    void dispatch(const Event& ev) const override {
        std::async(std::launch::async, [this, ev](){ EventDispatcher::dispatch(ev); });
    }
};

6. 小结

  • std::variant 提供了一个类型安全的多类型容器,适合用于事件系统的统一包装。
  • 结合 std::visit 与模板回调,我们能够在编译期解析事件类型,避免运行时类型检查的成本。
  • 通过简单的设计模式(观察者模式 + 事件总线),我们可以扩展到过滤器、异步分发等高级功能。

这个基于 std::variant 的事件系统既简单易用,又兼顾了性能与类型安全,是现代 C++ 项目中处理事件的理想方案。

发表评论