如何将不同签名的函数指针存储到同一个数组或std::unordered_map中?
实现方案说明
C++是静态类型语言,原生不支持不同签名函数直接无约束调用,但可以通过类型擦除实现你需要的存储+调用需求,以下是两种适配你场景的成熟方案:
方案1:基于std::any的轻量实现
这是最低成本的实现方案,核心是用std::any做类型擦除存储任意函数指针,调用时显式指定签名转换即可,适配任意返回值场景:
#include <unordered_map> #include <string> #include <any> #include <iostream> // 测试函数 void funcA(std::string str) { std::cout << "funcA调用:传入参数为" << str << std::endl; } void funcB(int32_t i, int32_t j) { std::cout << "funcB调用:参数和为" << i + j << std::endl; } int funcC(int a) { // 带返回值的测试函数 return a * 2; } int main() { std::unordered_map<std::string, std::any> func_map; // 存入任意签名的函数 func_map.emplace("funcA", &funcA); func_map.emplace("funcB", &funcB); func_map.emplace("funcC", &funcC); // 调用时显式转换为对应签名的函数指针 std::any_cast<void(*)(std::string)>(func_map["funcA"])("test"); std::any_cast<void(*)(int32_t, int32_t)>(func_map["funcB"])(3,4); int res = std::any_cast<int(*)(int)>(func_map["funcC"])(5); std::cout << "funcC返回值:" << res << std::endl; return 0; }
- 优点:无额外封装成本,支持存储任意可调用对象,适配所有返回值类型
- 缺点:调用时需要提前知道函数签名,无法直接实现你示例中的
map["funcA"]("test")语法,签名写错会抛出std::bad_any_cast异常
方案2:封装类型擦除类,实现目标调用语法
这是游戏开发中回调/事件管理器的常用方案,通过封装自定义可调用类,重载operator()实现你需要的直接调用效果,同时支持任意返回值:
#include <unordered_map> #include <string> #include <any> #include <functional> #include <stdexcept> #include <iostream> class AnyFunc { private: std::any m_func; public: // 支持任意可调用对象的构造 template<typename Func> AnyFunc(Func&& f) : m_func(std::forward<Func>(f)) {} // 重载调用运算符,自动匹配签名 template<typename... Args> decltype(auto) operator()(Args&&... args) const { // 匹配std::function类型 using FuncType = std::function<std::invoke_result_t<AnyFunc, Args...>(Args...)>; if (auto* f = std::any_cast<FuncType>(&m_func)) { return (*f)(std::forward<Args>(args)...); } // 匹配原生函数指针类型 using RawFuncType = std::invoke_result_t<AnyFunc, Args...>(*)(Args...); if (auto* f = std::any_cast<RawFuncType>(&m_func)) { return (*f)(std::forward<Args>(args)...); } throw std::runtime_error("回调函数签名不匹配"); } }; // 测试函数 void funcA(std::string str) { std::cout << "funcA调用:传入参数为" << str << std::endl; } void funcB(int32_t i, int32_t j) { std::cout << "funcB调用:参数和为" << i + j << std::endl; } float funcC(float a) { return a * 1.5f; } int main() { std::unordered_map<std::string, AnyFunc> func_map; func_map.emplace("funcA", &funcA); func_map.emplace("funcB", &funcB); func_map.emplace("funcC", &funcC); // 完全符合你要求的调用语法 func_map["funcA"]("test"); func_map["funcB"](3,4); float res = func_map["funcC"](2.0f); std::cout << "funcC返回值:" << res << std::endl; return 0; }
- 优点:完全匹配你需要的调用写法,支持任意签名/返回值的可调用对象,可扩展错误处理、参数校验逻辑,非常适合游戏事件系统场景
- 缺点:签名不匹配为运行时报错,你可以通过约定同一事件的参数格式规避这个问题
游戏事件管理器的优化建议
如果是做商业级游戏的事件系统,更推荐按事件类型分类存储回调:给每个事件定义固定的参数结构体,同一事件的回调签名保持一致,既可以做到编译期类型安全,也没有运行期类型转换的性能开销,更适配游戏的高性能要求。
内容的提问来源于stack exchange,提问作者a a
相关产品推荐
相关产品推荐

