记一次ADL导致的C++代码编译错误
好了下面我们进入正题吧。偶遇报错最近工作收尾有了不少空闲时间于是准备试试手头环境的编译器对新标准的支持以便选择合适的时机给自己的几个项目做个升级。虽然有现成的工具的网站可以查询编译器对新标准的支持情况但这些网站给的信息还是不够详细有时候得写些例子手动编译做测试。我是个懒人所以我不愿意花时间自己写而AI又对新标准理解的不够透彻可能是语料太少的缘故总是写出点离谱的东西。无奈之下我只能去网上找现成的吃了cppreference是个不错的选择用的人很多而且比较权威更棒的是对于新特性它一般都给出了示例代码这正中我的下怀。于是我搬了这样一段代码进行测试预想中要么编译成功要么新特性不支持导致编译失败#include array#include iostream#include list#include ranges#include string#include tuple#include vectorvoid print(auto const rem, auto const range){for (std::cout rem; auto const elem : range)std::cout elem ;std::cout \n;}int main(){auto x std::vector{1, 2, 3, 4};auto y std::liststd::string{α, β, γ, δ, ε};auto z std::array{A, B, C, D, E, F};print(Source views:, );print(x: , x);print(y: , y);print(z: , z);print(\nzip(x,y,z):, );for (std::tupleint, std::string, char elem : std::views::zip(x, y, z)){std::cout std::get0(elem) std::get1(elem) std::get2(elem) \n;std::getchar(elem) (a - A); // modifies the element of z}print(\nAfter modification, z: , z);}很简单的代码测试一下c23的ranges::views::zip如果要报错那么多半也是和这个zip有关。然而事实出人意料$ clang -stdc23 -Wall test.cpptest.cpp:23:5: error: call to print is ambiguous23 | print(x: , x);| ^~~~~test.cpp:9:6: note: candidate function [with rem:auto const char *, range:auto std::vectorint]9 | void print(auto const rem, auto const range)| ^/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c/v1/print:343:28: note: candidate function [with _Args std::vectorint ]343 | _LIBCPP_HIDE_FROM_ABI void print(format_string_Args... __fmt, _Args... __args) {| ^test.cpp:24:5: error: call to print is ambiguous24 | print(y: , y);| ^~~~~test.cpp:9:6: note: candidate function [with rem:auto const char *, range:auto std::liststd::string]9 | void print(auto const rem, auto const range)| ^/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c/v1/print:343:28: note: candidate function [with _Args std::liststd::string ]343 | _LIBCPP_HIDE_FROM_ABI void print(format_string_Args... __fmt, _Args... __args) {| ^test.cpp:25:5: error: call to print is ambiguous25 | print(z: , z);| ^~~~~test.cpp:9:6: note: candidate function [with rem:auto const char *, range:auto std::arraychar, 6]9 | void print(auto const rem, auto const range)| ^/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c/v1/print:343:28: note: candidate function [with _Args std::arraychar, 6 ]343 | _LIBCPP_HIDE_FROM_ABI void print(format_string_Args... __fmt, _Args... __args) {| ^test.cpp:38:5: error: call to print is ambiguous38 | print(\nAfter modification, z: , z);| ^~~~~test.cpp:9:6: note: candidate function [with rem:auto const char *, range:auto std::arraychar, 6]9 | void print(auto const rem, auto const range)| ^/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c/v1/print:343:28: note: candidate function [with _Args std::arraychar, 6 ]343 | _LIBCPP_HIDE_FROM_ABI void print(format_string_Args... __fmt, _Args... __args) {| ^4 errors generated.print函数报错了和zip完全不相关难道说cppreference上例子会有这么明显的错误但检查了一下print也只用到了早就支持的c20的语法并不存在错误而且换成gcc和Linux上的clang18之后都能正常编译。这还只是第一个点异常仔细阅读报错信息就会发现第二点了我们没有导入c23的新标准库print为什么我们自定义的print会和std::print冲突呢看到这里是不是已经按耐不住自己想转投Rust的心了不过别急尽管报错很离奇但原因没那么复杂听我慢慢解释。基础回顾基础回顾是c博客少不了的环节因为语法太多太琐碎不回顾下容易看不懂后续的内容。限定和非限定名称第一个要回顾的是限定名称和非限定名称这两个概念。国内有时候也会把非限定名称叫做无限定名称我觉得后者更符合中文的语用习惯不过我这儿一直非限定非限定的习惯了所以就不改了。如果要照着标准规范念经那可有得念了所以我会有通俗易懂的方式解释这样多少会和真正的标准有那么点出入还请语言律师们海涵。简单的说c里如果一个标识符光秃秃的比如print那么它是非限定名称而如果一个名字前面包含命名空间限定符比如::print, std::print, classA::print那么它是限定名称。他俩有啥区别呢限定名称的限定指的是指定了这标识符出现在那个命名空间/类里编译器只能去限定的地方查找没找到就是编译错误。而非限定名称因为没限制编译器去哪找这个标识符所以编译器会从当前作用域开始一路往上走查找每个父作用域/类以找到这个标识符注意同级的命名空间/类不会进行搜索。举个例子#include iostreamnamespace A {int a 1;int b 2;namespace B {int b 3;void print(){std::cout b \n; // 非限定名称就近找到A::B::bstd::cout a \n; // 非限定名称找到父命名空间的A::astd::cout A::b \n; // 限定名称直接找到A::b// 下面这行会报错因为使用了限定名称只允许编译器搜索BB中没有a// std::cout B::a \n;}}}int main(){A::B::print(); // 这也是限定名称// 输出 3 1 2}顺带一提每个编译单元都有一个默认存在的匿名的命名空间所有没有明确定义在其他命名空间中的标识符都会被归入这个匿名的命名空间。举个例子前文里我们定义的print函数就是在这个匿名的命名空间中这个空间和std是平级关系。非限定名称可以让程序员以自然的方式引入外层作用域的名字而限定名称则提供了一个防止名称冲突的机制。ADL理解了限定和非限定名称下面我们再看看这行代码std::cout A::b \n;注意那个c允许进行运算符重载所以它的真身其实是std::ostream operator(...)并且这个运算符是定义在std这个命名空间中的。因为我们没有限定运算符的命名空间按照运算符当前的调用方式我们也没法进行限定所以编译器会从当前作用域开始逐层往上查找。但我们的代码中没有定义过这个运算符std则不在非限定名称的搜索范围内理论上编译器不应该报错说找不到operator吗事实上程序可以正常编译因为c还有另外一套名称查找策略叫ADL——Argument Dependent Lookup。简单的说如果一个函数/运算符是非限定名称而它的实际参数的类型所在的命名空间里定义有同名的函数那么编译器就会把这个和实参类型在同一空间的函数当成这个非限定名称指代的函数/运算符。当然真实环境下编译器还得考虑可见性和函数重载决议这里我们不细究了。还是以上面那行代码为例虽然我们没有重载但iostream里有在std里重载而我们的实际参数是std::cout类型是std::ostream所以ADL会去命名空间std中查找是否有符合调用形式的operator编译器会发现正好有完全合适的运算符存在所以编译成功不会报错。另外ADL只适用于函数和运算符也算一种特殊的函数lambda、functor等东西触发不了ADL。ADL最大的用处是方便了运算符重载的使用。否则我们不得不写很多std::operator(a, b)这样的代码这既繁琐又不符合自然习惯。此外c还有一些基于ADL的惯用法例如我之前介绍过的copy-and-swap惯用法。不过除了少数正面作用ADL更多的时候是个trouble maker本文开头那个报错就是活生生的例子。报错原因复习完基础我们再看报错信息test.cpp:23:5: error: call to print is ambiguous23 | print(x: , x);| ^~~~~test.cpp:9:6: note: candidate function [with rem:auto const char *, range:auto std::vectorint]9 | void print(auto const rem, auto const range)| ^/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.pl