You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用std::visit调用std::variant存储的std::function?

解决std::variant存储多签名std::function的调用问题

好问题!你选std::visit来处理std::variant完全正确,核心问题在于你需要针对每个variant中存储的std::function的签名,提供对应的调用逻辑——因为你的var_t包含两种不同签名的函数:无参数的void()和单int参数的void(int),所以访问器必须能处理这两种情况。

问题分析

你的代码里有两个小问题:

  1. callbacks.at(E)会在事件枚举对应的键不存在时抛出std::out_of_range异常,订阅时应该自动创建对应的vector;
  2. 原std::visit的访问器没有区分函数签名,直接调用无参数版本会导致void(int)类型的函数编译失败。

修正后的完整代码

#include <variant>
#include <functional>
#include <map>
#include <iostream>
#include <type_traits> // 用于编译期判断函数可调用性

using var_t = std::variant<std::function<void(void)>, std::function<void(int)>>;
enum class EventEnum { A, B, C };

struct Controller {
    template<EventEnum E>
    void subscribe(var_t fn) {
        // 改用operator[],自动创建不存在的事件对应的回调vector
        auto& callbacksVec = callbacks[E];
        callbacksVec.push_back(fn);
    }

    // 无参数版本的notify:给需要int参数的回调传默认值
    template<EventEnum E>
    void notify() {
        auto& callbacksVec = callbacks[E];
        for (auto& func : callbacksVec) {
            std::visit([](auto&& fn) {
                using FnType = std::decay_t<decltype(fn)>;
                // 编译期判断函数是否可无参数调用
                if constexpr (std::is_invocable_v<FnType>) {
                    std::invoke(fn);
                }
                // 编译期判断函数是否可接收int参数调用
                else if constexpr (std::is_invocable_v<FnType, int>) {
                    std::invoke(fn, 0); // 可根据业务调整默认参数
                }
            }, func);
        }
    }

    // 带int参数的notify:给需要参数的回调传指定值
    template<EventEnum E>
    void notify(int value) {
        auto& callbacksVec = callbacks[E];
        for (auto& func : callbacksVec) {
            std::visit([value](auto&& fn) {
                using FnType = std::decay_t<decltype(fn)>;
                if constexpr (std::is_invocable_v<FnType>) {
                    std::invoke(fn); // 无参数回调忽略传入的参数
                }
                else if constexpr (std::is_invocable_v<FnType, int>) {
                    std::invoke(fn, value);
                }
            }, func);
        }
    }

    std::map<EventEnum, std::vector<var_t>> callbacks;
};

int main() {
    auto fn_no_arg = []() { std::cout << "无参数回调触发!" << std::endl; };
    auto fn_with_int = [](int x) { std::cout << "带int参数的回调触发,参数值:" << x << std::endl; };

    Controller myController;
    myController.subscribe<EventEnum::A>(fn_no_arg);
    myController.subscribe<EventEnum::A>(fn_with_int);

    std::cout << "=== 调用无参数notify ===" << std::endl;
    myController.notify<EventEnum::A>();

    std::cout << "\n=== 调用带参数notify(42) ===" << std::endl;
    myController.notify<EventEnum::A>(42);

    return 0;
}

关键知识点

  1. std::visit的访问器必须覆盖所有variant类型:
    用if constexpr配合std::is_invocable_v可以在编译期判断函数的签名,选择正确的调用方式,避免运行时错误。
  2. std::map的operator[] vs at():
    operator[]会自动为不存在的键创建默认构造的vector,适合订阅场景;at()仅用于访问已存在的键,否则抛出异常。
  3. 灵活的notify重载:
    可以根据事件的实际需求,提供不同参数版本的notify,让不同签名的回调都能被正确触发。

额外优化建议

如果你的业务中同一个事件的回调签名是统一的,可以进一步优化设计:比如用模板特化将事件枚举和回调签名绑定,避免同一个vector中混存不同签名的函数,这样调用时可以更简洁,无需分支判断。

内容的提问来源于stack exchange,提问作者Fluffy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:51:56