C++静态变量两阶段查找:static_assert失败assert通过原因
问题参考代码
#include <iostream> #include <cassert> using std::cout; using std::endl; namespace tpl_ { template<class T> constexpr int test(T) { return 1;} template<class T> struct check { static T const t; static constexpr auto val = test(T{}); template <typename Y = T> void func() { int val = test(Y{}); assert(val == 1); } }; constexpr int test(int) { return 2; } } //tlp_ int main() { static_assert(tpl_::check<int>::val == 2); }
上述代码编译时会直接触发static_assert失败,而如果实例化调用check<int>{}.func(),内部的assert(val == 1)可以正常通过。
核心原因
这个结果完全符合C++两阶段查找的规则,偏差来自于对两阶段查找的候选集收集逻辑、ADL(参数依赖查找)适用边界的误解,具体拆解:
- 首先纠正一个常见的认知错误:两阶段查找从来不是“依赖名就推迟到第二阶段找全所有可见名字”,它的固定规则是:
- 第一阶段在模板的定义点执行:对非限定的函数调用名做常规的作用域向上查找,这一步找到的所有重载会固定进入候选集,后续不会因为作用域出现新重载被替换。
- 第二阶段在模板的实例化点执行:只会额外收集通过ADL规则找到的、实例化点可见的重载加入候选集,不会再做普通的跨作用域非ADL查找。简单说,定义点之后、实例化点之前在同一命名空间下声明的普通重载,只要走不通ADL,永远不会进入候选集。
- 再解释为什么两个位置的
test调用都拿到了返回值1:
两个位置的调用形式都是test(模板参数类型{}),当模板参数替换为int时,传入的实参是C++内置类型int。标准明确规定,内置类型的关联命名空间仅为全局命名空间,关联类集合为空,ADL查找不会进入tpl_这种用户自定义命名空间找重载。
也就是说,第二阶段查找时,虽然tpl_::test(int)这个返回2的重载已经在实例化点可见,但它不在ADL的查找范围内,根本进不了重载决议的候选集。两个位置的调用都只能使用第一阶段找到的泛型版本template<class T> constexpr int test(T),返回值自然都是1。 - 最后澄清容易混淆的查找时机差异:
类模板的静态数据成员初始化、类模板的普通成员函数/成员函数模板内的依赖名查找,遵循完全一致的两阶段查找规则,不存在“静态成员不做延迟查找、成员函数延迟查找就能找全所有可见重载”的区别。func里的断言能过,本质是它拿到的返回值本来就是1,刚好匹配断言条件,不是因为它找到了返回2的重载。
可以自行验证:把
constexpr int test(int) { return 2; }的声明移动到check类定义的前面,第一阶段普通查找就能找到这个更匹配的int重载,此时check<int>::val会等于2,static_assert可以正常通过,反而是func内的断言会因为拿到返回值2触发失败。
内容的提问来源于stack exchange,提问作者PYA
相关产品推荐
相关产品推荐

