如何使用std::visit调用std::variant存储的std::function?
解决
std::variant存储多签名std::function的调用问题 好问题!你选std::visit来处理std::variant完全正确,核心问题在于你需要针对每个variant中存储的std::function的签名,提供对应的调用逻辑——因为你的var_t包含两种不同签名的函数:无参数的void()和单int参数的void(int),所以访问器必须能处理这两种情况。
问题分析
你的代码里有两个小问题:
callbacks.at(E)会在事件枚举对应的键不存在时抛出std::out_of_range异常,订阅时应该自动创建对应的vector;- 原
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; }
关键知识点
std::visit的访问器必须覆盖所有variant类型:
用if constexpr配合std::is_invocable_v可以在编译期判断函数的签名,选择正确的调用方式,避免运行时错误。std::map的operator[]vsat():operator[]会自动为不存在的键创建默认构造的vector,适合订阅场景;at()仅用于访问已存在的键,否则抛出异常。- 灵活的
notify重载:
可以根据事件的实际需求,提供不同参数版本的notify,让不同签名的回调都能被正确触发。
额外优化建议
如果你的业务中同一个事件的回调签名是统一的,可以进一步优化设计:比如用模板特化将事件枚举和回调签名绑定,避免同一个vector中混存不同签名的函数,这样调用时可以更简洁,无需分支判断。
内容的提问来源于stack exchange,提问作者Fluffy
相关产品推荐
相关产品推荐

