C++11 std::result_of和C++17 invoke_result如何工作?为何实现复杂?
你的简单版本的核心局限
你的简化版本本质是实现了类似C++17 std::invoke_result的参数形式(直接传入可调用对象类型和参数类型),但和C++11引入的std::result_of标准接口并不一致——标准std::result_of的模板参数是函数类型签名(比如result_of<F(Args...)>),而非拆分的F和Args...。除此之外,这个版本还存在几个关键缺陷:
- 无法处理成员函数指针、成员数据指针这类特殊可调用类型:直接调用
std::declval<F>()对成员函数指针是非法的,必须通过对象实例(指针/引用)才能调用。 - 不支持SFINAE:当调用表达式非法时,你的版本会直接触发编译错误,而标准
std::result_of需要在这种情况下不定义type成员,允许通过SFINAE在模板重载中排除无效情况。 - 忽略cv/引用限定:比如带有
const/volatile限定的成员函数、或带引用限定的operator(),你的版本无法正确推导返回类型。
标准实现需要处理的复杂场景
主流编译器(GCC/LLVM)和cppreference的参考实现,核心是要覆盖所有C++可调用类型的调用语义,同时符合标准的接口和SFINAE要求,主要需要处理以下场景:
1. 拆解标准要求的模板参数形式
标准std::result_of要求模板参数是Fn(ArgTypes...)的函数类型签名,所以第一步需要通过模板特化拆解出可调用对象类型Fn和参数类型ArgTypes...。比如:
// 主模板,无type成员 template<typename> struct result_of; // 特化:匹配函数类型签名,拆解Fn和Args... template<typename Fn, typename... Args> struct result_of<Fn(Args...)> { // 内部实现调用检测和类型推导 };
2. 适配所有可调用类型的调用逻辑
C++的可调用类型包括普通函数、函数指针、函数对象(带operator()的类)、成员函数指针、成员数据指针,每种类型的调用语法都不同:
- 普通函数/函数对象:直接调用
std::declval<Fn>()(std::declval<Args>()...) - 成员函数指针:需要通过对象实例调用,比如
(std::declval<Obj>()).*std::declval<MemFn>()(std::declval<Args>()...)(对象引用)或std::declval<Obj*>()->*std::declval<MemFn>()(...)(对象指针) - 成员数据指针:本质是获取成员值,语法为
std::declval<Obj>()->*std::declval<MemData>()或(std::declval<Obj>()).*std::declval<MemData>()
3. 支持SFINAE的合法调用检测
为了在调用表达式非法时(比如参数不匹配、可调用类型不支持该参数)不触发编译错误,而是让std::result_of无type成员,实现中会用SFINAE技术检测调用表达式的合法性:
通常会借助辅助模板,用decltype包裹调用表达式,结合std::enable_if或模板偏特化来判断表达式是否合法,只有合法时才定义type成员。
4. 处理cv/引用限定的细节
对于带const/volatile限定的成员函数,或带左值/右值引用限定的operator(),需要确保推导的返回类型符合实际调用的语义。比如调用const成员函数时,对象实例必须是const限定的,否则调用非法。
为何实现如此复杂?
核心原因是C++的可调用类型体系本身就十分复杂,而std::result_of需要:
- 严格符合标准的接口设计(函数类型签名作为模板参数)
- 统一兼容所有可调用类型的调用语法
- 支持SFINAE以适配模板元编程中的条件分支
- 处理各种cv、引用限定的边缘情况,保证类型推导的正确性
后来C++17引入std::invoke_result就是为了简化std::result_of的接口(直接传入可调用类型和参数类型,无需包装成函数签名),同时复用std::invoke的统一调用逻辑,解决std::result_of的接口混淆问题。
内容的提问来源于stack exchange,提问作者edKotinsky

