SFINAE与requires函数模板绑定规则差异及行为不一致原因
问题描述
我原本使用带requires关键字的检查函数检测目标函数是否定义,为适配C++17改用表达式SFINAE实现检查器,却发现了明显差异:
- 先调用两个检查器(此时目标函数
Foo未定义),定义Foo后再次检查 requires检查器会复用旧结果,仍然显示Foo未定义- SFINAE检查器会更新结果,正确显示
Foo已定义
两者均使用依赖类型作为返回类型,为何行为不同?同时想了解,为何模板参数不变时,函数模板的实例解释仍会有所不同?
测试代码如下:
// Compiles on Clang, GCC, and MSVC. #include <type_traits> struct MyStruct {}; constexpr bool Checker_SFINAE(...) { return false; } template <typename T> constexpr auto Checker_SFINAE(T) -> decltype(Foo(T()), bool()) { return true; } template <typename T> constexpr auto Checker_requires(T) { return std::bool_constant<requires { Foo(T()); }>(); } static_assert(!Checker_SFINAE(MyStruct())); static_assert(!Checker_requires(MyStruct())); void Foo(MyStruct) {} static_assert(Checker_SFINAE(MyStruct())); // Will fail is the previous Checker_requires is removed static_assert(!Checker_requires(MyStruct())); int main(){}
核心原因分析
1. Checker_requires复用旧实例的本质
Checker_requires是一个函数模板,当第一个static_assert触发Checker_requires<MyStruct>的实例化时,Foo(MyStruct)还未定义。此时模板内部的requires { Foo(T()); }表达式会基于当前可见的声明求值,结果为false,因此实例化出的函数返回std::bool_constant<false>。
C++标准规定,函数模板的特化一旦实例化完成,后续使用相同模板参数的调用会直接复用这个已生成的特化,不会重新分析模板内部的代码。所以即使后面定义了Foo,再次调用Checker_requires(MyStruct())时,编译器不会重新计算requires表达式,直接返回之前实例化好的false结果。
如果删除第一个Checker_requires的static_assert,第二个static_assert会在Foo定义后才触发实例化,此时requires表达式求值为true,断言就会通过。
2. Checker_SFINAE能更新结果的原因
Checker_SFINAE基于重载决议和SFINAE机制工作:
- 第一次调用时,
Foo未定义,模板版本的Checker_SFINAE<T>因为decltype(Foo(T()), bool())中的Foo(T())无有效声明,会被SFINAE规则淘汰,编译器只能选择可变参数的Checker_SFINAE(...),返回false。 - 当
Foo定义后,第二次调用时,模板版本的重载变得有效(Foo(T())此时有合法声明),编译器会优先选择这个更匹配的模板重载,返回true。
这里的关键是:两次调用选择的是不同的重载函数,而非同一个模板特化的复用。重载决议是在每个调用点独立进行的,会考虑调用点处可见的所有函数声明,因此能感知到Foo定义后的变化。
3. 模板参数不变但表现不同的本质
这个问题的核心不是“同一个模板特化的解释不同”,而是两种检查器的实现机制完全不同:
Checker_requires依赖模板实例化的一次性特性:同一个模板参数的特化只会被实例化一次,实例化时的上下文(比如Foo是否定义)决定了特化的行为,后续无法更改。Checker_SFINAE依赖重载决议的即时性:每次调用都会重新进行重载匹配,调用点的上下文变化(Foo的定义)会直接影响哪个重载被选中。
此外,函数模板只有当模板参数或者模板依赖的其他实体发生变化时,才会生成新的特化。在Checker_requires的场景中,模板参数T始终是MyStruct,所以不会触发新的实例化,自然不会更新结果。
内容的提问来源于stack exchange,提问作者Joshua Maiche

