为何非类型模板参数不考虑ADL?求风险示例
先搞懂第一个问题:非类型模板参数和ADL的本质矛盾
ADL(实参依赖查找)的核心逻辑其实很简单:编译器会根据你传给函数的实参的类型,自动去这个类型所属的命名空间里找匹配的函数。比如你传一个N::MyClass对象给函数,编译器会自动搜N命名空间里的函数,不用你手动写N::func。
但非类型模板参数是值,不是类型。哪怕这个值来自某个命名空间(比如你代码里的N::c),它的类型是int——这是个全局内置类型,和N半毛钱关系都没有。ADL根本不关心“这个值是从哪个命名空间拿的”,它只认实参的类型对应的命名空间。
你之前猜“运算符重载不用非类型模板参数”,其实不对——运算符重载完全可以用非类型模板参数,但ADL触发与否和是不是运算符无关,只看实参的类型。比如:
namespace N { struct MyStruct {}; template <int X> bool operator==(MyStruct, decltype(X)) { return true; } } int main() { N::MyStruct{} == 5; // ADL会触发,因为第一个实参是N::MyStruct类型,所以能找到N::operator== }
这里的非类型模板参数X没影响ADL,真正让ADL生效的是N::MyStruct这个类型的实参。
你的代码为啥报错?
看你写的代码:
namespace N { const int c = 1; void func(...) {} } template <int r> void tfunc () {} int main () { func(tfunc<N::c>); // Error! }
这里你传给func的实参是tfunc<N::c>,它的类型是void (*)()——也就是一个指向无参无返回函数的指针,这个类型和N命名空间没有任何关联。ADL不会因为你用了N::c当模板参数,就去N里找func。编译器只会在全局命名空间搜func,但全局没定义,所以直接报错。
要解决这个问题,要么显式写N::func(tfunc<N::c>),要么把func的实参改成和N有关联的用户定义类型(内置类型不行,因为内置类型没有所属命名空间),这样ADL才会触发:
namespace N { struct S {}; void func(S) {} } int main() { N::func(tfunc<N::c>); // 显式指定命名空间,没问题 func(N::S{}); // ADL触发,自动找到N::func }
为啥允许非类型模板参数触发ADL是烂设计?
假设C++真的允许根据非类型模板参数的来源命名空间触发ADL,会搞出一堆混乱的问题,给你举个直观的例子:
namespace 公制单位 { constexpr double π = 3.14159; void 计算圆面积(double 半径) { std::cout << "公制面积:" << π * 半径 * 半径 << " 平方米\n"; } } namespace 英制单位 { constexpr double π = 3.14159; // 和公制的π值一样,但属于不同命名空间 void 计算圆面积(double 半径) { std::cout << "英制面积:" << π * 半径 * 半径 << " 平方英寸\n"; } } template <double 圆周率> void 打印圆面积(double 半径) { 计算圆面积(半径); // 如果ADL根据圆周率的来源命名空间找函数... } int main() { 打印圆面积<公制单位::π>(5.0); // 会调用公制的计算函数? 打印圆面积<英制单位::π>(5.0); // 会调用英制的计算函数? 打印圆面积<3.14159>(5.0); // 字面量3.14159属于哪个命名空间?编译器完全懵了! }
你看,同一个模板打印圆面积的行为,居然完全依赖于模板参数的来源命名空间,而不是它的类型或值。更离谱的是字面量的情况——3.14159没有所属的命名空间,编译器根本不知道该搜哪个命名空间找计算圆面积,直接就会编译错误或者出现歧义。
再想想,如果两个命名空间有同名常量但不同值,模板的行为会变得完全不可预测,你得时刻记住每个常量来自哪个命名空间,这完全违背了模板“通用、一致”的设计初衷。
说白了,ADL是为类型关联设计的,而非类型模板参数是值,没有类型关联的属性;允许值的来源触发ADL会把代码搞得一团糟,破坏可预测性和可维护性,所以C++标准直接禁止了这种行为。
内容的提问来源于stack exchange,提问作者Baykov Nikita

