在std::any中存储成员函数指针的签名异常问题及解决方法
问题背景
在实现成员函数映射时,发现将成员函数指针存入std::any后,调用阶段的类型签名与存储阶段不一致,导致无法通过std::any_cast调用目标函数。
测试代码
#include <any> #include <iostream> #include <map> class VarCall { public: VarCall() : calls({{"Alice", std::any(&VarCall::foo)}, {"Bob", std::any(&VarCall::bar)}, {"Charlie", std::any(&VarCall::baz)}}) {} template <typename... Args> void call(const std::string& what, Args... args) { void (VarCall::*ptr)(Args...); std::any a = ptr; std::cout << a.type().name() << std::endl; std::cout << calls[what].type().name() << std::endl; // 调用尝试失败 // this->*(std::any_cast<decltype(ptr)>(calls[what]))(args...); } public: void foo() { std::cout << "foo()" << std::endl; } void bar(const std::string& s) { std::cout << "bar(" << s << ")" << std::endl; } void baz(int i) { std::cout << "baz(" << i << ")" << std::endl; } std::map<std::string, std::any> calls; }; int main() { VarCall v; void (VarCall::*ptr)(const std::string& s); std::any a = ptr; std::any b(&VarCall::bar); std::cout << a.type().name() << std::endl; std::cout << b.type().name() << std::endl; // v.call("Alice"); v.call("Bob", "2"); // v.call("Charlie", 1); return 0; }
预期输出
M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE
M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE
M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE
M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE
实际输出
M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE
M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE
M7VarCallFvPKcE
M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE
原因分析
- 模板参数推导的自动匹配:调用
v.call("Bob", "2")时,字符串字面量"2"的类型是const char*,模板参数Args...会被自动推导为const char*,因此call函数内的ptr类型是void (VarCall::*)(const char*),对应输出中的M7VarCallFvPKcE。 - 存储阶段的类型正确性:构造函数中存入
std::any的&VarCall::bar类型是void (VarCall::*)(const std::string&),这与main函数中b.type().name()的输出一致,说明存储阶段的类型是正确的。 - 类型不匹配导致调用失败:
std::any_cast要求目标类型与存储的类型完全一致,推导后的const char*版本指针与存储的const std::string&版本指针类型不兼容,因此无法完成转换和调用。
解决方案
方案一:显式指定模板参数
调用call时显式指定参数类型为const std::string&,让模板参数与存储的成员函数指针类型匹配:
v.call<const std::string&>("Bob", "2");
此时ptr的类型会被指定为void (VarCall::*)(const std::string&),与std::any中存储的类型一致,即可成功执行std::any_cast调用。
方案二:使用std::function包装成员函数
将成员函数包装为std::function存入映射,利用std::function的隐式参数转换能力(如const char*自动转为std::string),避免严格的类型匹配限制:
#include <any> #include <iostream> #include <map> #include <functional> class VarCall { public: VarCall() : calls({{"Alice", std::function<void(VarCall&)>([](VarCall& obj) { obj.foo(); })}, {"Bob", std::function<void(VarCall&, const std::string&)>([](VarCall& obj, const std::string& s) { obj.bar(s); })}, {"Charlie", std::function<void(VarCall&, int)>([](VarCall& obj, int i) { obj.baz(i); })}}) {} template <typename... Args> void call(const std::string& what, Args... args) { auto func = std::any_cast<std::function<void(VarCall&, Args...)>>(calls[what]); func(*this, std::forward<Args>(args)...); } public: void foo() { std::cout << "foo()" << std::endl; } void bar(const std::string& s) { std::cout << "bar(" << s << ")" << std::endl; } void baz(int i) { std::cout << "baz(" << i << ")" << std::endl; } std::map<std::string, std::any> calls; }; int main() { VarCall v; v.call("Bob", "2"); // 自动将const char*转为std::string return 0; }
这种方式更灵活,无需显式指定模板参数,std::function会自动处理参数的隐式转换,简化调用逻辑。
内容的提问来源于stack exchange,提问作者Oersted

