“参数相关查找”(又称 ADL 或“Koenig 查找”)是什么?

219
有关参数依赖查找(argument dependent lookup),有哪些好的解释呢?许多人也称其为Koenig Lookup。
最好能够了解以下内容:
- 它为什么是好事? - 它为什么是坏事? - 它是如何工作的?

2
http://www.gotw.ca/gotw/030.htm - Flexo
11
这是一件好事,因为否则 std::cout << "Hello world"; 将无法编译。 - sehe
http://en.cppreference.com/w/cpp/language/adl - Li Kui
4个回答

281

Koenig查找,或称为参数依赖查找,描述了C++中编译器如何查找未限定名称。

C++11标准§3.4.2/1规定:

当函数调用(5.2.2)中的后缀表达式是未限定标识符时,在常规未限定查找(3.4.1)期间未考虑的其他命名空间可能会被搜索,并且在这些命名空间中,可以找到命名空间作用域友元函数声明(11.3),否则不可见。这些搜索的修改取决于参数的类型(对于模板模板参数,还取决于模板参数的命名空间)。

用更简单的话来说,Nicolai Josuttis指出1

如果一个或多个参数类型在函数的命名空间中定义,则不必为函数限定命名空间。

下面是一个简单的代码示例:

namespace MyNamespace
{
    class MyClass {};
    void doSomething(MyClass) {}
}

MyNamespace::MyClass obj; // global object


int main()
{
    doSomething(obj); // Works Fine - MyNamespace::doSomething() is called.
}

在上面的示例中,既没有using声明也没有using指令,但编译器仍然通过应用Koenig查找正确地将未限定名称doSomething()识别为命名空间MyNamespace中声明的函数。

它是如何工作的?

该算法告诉编译器不仅要查看本地作用域,还要查看包含参数类型的命名空间。因此,在上述代码中,编译器发现函数doSomething()的参数obj属于命名空间MyNamespace。因此,它查看该命名空间以定位doSomething()的声明。

Koenig查找的优点是什么?

正如上面的简单代码示例所示,Koenig查找为程序员提供了方便和易用性。如果没有Koenig查找,程序员将需要重复指定完全限定名称,或者使用大量using声明。

为什么会批评Koenig查找?

过度依赖Koenig查找可能会导致语义问题,并有时使程序员措手不及。

考虑std::swap的示例,它是一个标准库算法,用于交换两个值。使用Koenig查找时,使用此算法时必须小心,因为:

std::swap(obj1,obj2);

可能表现不同于:

using std::swap;
swap(obj1, obj2);

使用ADL,调用哪个swap函数取决于传递给它的参数的命名空间。

如果存在一个命名空间A,并且如果A::obj1A::obj2A::swap()存在,则第二个示例将导致调用A::swap(),这可能不是用户想要的。

此外,如果由于某种原因同时定义了A::swap(A::MyClass&, A::MyClass&)std::swap(A::MyClass&, A::MyClass&),则第一个示例将调用std::swap(A::MyClass&, A::MyClass&),但第二个示例将无法编译,因为swap(obj1, obj2)会产生歧义。

趣闻:

为什么称其为“Koenig查找”?

因为它是由前AT&T和贝尔实验室研究员和程序员Andrew Koenig设计的。

进一步阅读:


**1** Koenig查找的定义如Josuttis的书《C++标准库:教程与参考》中所定义。

15
@AlokSave:赞同这个答案,但是这个小知识点并不正确。Koenig并没有发明ADL,他在这里承认了这一点 :) - legends2k
27
在批评Koenig算法的例子中,可以将其视为Koenig查找的“特点”和“缺点”。在这种方式中使用std::swap()是一种常见的习惯用法:提供一个“using std::swap()”,以防未提供更专业的版本A::swap()。如果有A::swap()的专门版本可用,我们通常会希望调用那个版本。这为swap()调用提供了更多的一般性,因为我们可以相信调用会被编译并正常工作,但如果有更专业的版本存在,我们也可以相信会使用它。 - Anthony Hall
6
这里还有更多内容。使用 std::swap 时,你必须这样做,因为唯一的选择是为你的 A 类添加 std::swap 模板函数的显式特化。但是,如果你的 A 类本身就是一个模板,则它将是部分特化而不是显式特化。而且,模板函数的部分特化是不允许的。添加 std::swap 的重载可能是一个替代方案,但明确禁止这样做(你不能向 std 命名空间添加东西)。因此,ADL 是 std::swap唯一方法。 - Adam Badura
2
我本来期望在“koenig查找的优点”下看到对重载运算符的提及。使用std::swap()的例子似乎有些反向。我认为问题应该出现在选择std::swap()而不是特定于类型的重载A::swap()时。使用std::swap(A::MyClass&, A::MyClass&)的例子似乎是具有误导性的。由于std永远不会针对用户类型进行特定的重载,因此我认为这不是一个很好的例子。 - Arvid
错误:prog.cpp:(.text.startup+0x7):对 MyNamespace::doSomething(MyNamespace::MyClass)' 的引用未定义。` - gsamaras
1
@gsamaras ...然后呢?我们都能看到该函数从未被定义。实际上,你的错误信息证明它起作用了,因为它正在寻找MyNamespace :: doSomething,而不仅仅是::doSomething - anon

89
在Koenig Lookup中,如果调用函数时没有指定其命名空间,则函数名称也会在定义参数类型的命名空间中搜索。这就是为什么它也被称为Argument-Dependent name Lookup,简称ADL
正因为有了Koenig Lookup,我们才能写出这样的代码:
std::cout << "Hello World!" << "\n";

否则,我们必须写成:
std::operator<<(std::operator<<(std::cout, "Hello World!"), "\n");

这实在是太繁琐了,代码看起来也很丑陋!

换句话说,在没有Koenig Lookup的情况下,即使是一个Hello World程序看起来也很复杂。


11
请注意,std::cout 是函数的一个参数,这已足以启用 ADL。你有注意到吗? - Nawaz
1
@meet:你的问题需要一份较长的答案,这里无法提供。我只能建议你阅读以下主题:1)ostream<< 的签名(即它接受什么参数和返回什么)。2)完全限定名称(如 std::vectorstd::operator<<)。3)更详细的论述参数依赖查找。 - Nawaz
1
代码有漏洞: std::operator<<(std::operator<<(std::cout, s), std::endl); 应该改为 std::operator<<(std::cout, s).operator<<(std::endl);, 参见 http://ideone.com/FFKA7b - WorldSEnder
2
@WorldSEnder:是的,你说得对。可以将std::endl作为参数传递的函数实际上是一个成员函数。无论如何,如果我使用"\n"代替std::endl,那么我的答案就是正确的。感谢您的评论。 - Nawaz
2
@Destructor: 因为形如 f(a,b) 的函数调用会调用一个自由函数。因此,在 std::operator<<(std::cout, std::endl); 的情况下,没有接受 std::endl 作为第二个参数的自由函数。它是接受 std::endl 作为参数的成员函数,你需要编写 std::cout.operator<<(std::endl);。而且,由于有一个接受 char const* 作为第二个参数的自由函数,所以 "\n" 可以工作;'\n' 也可以工作。 - Nawaz
显示剩余4条评论

35
也许最好从为什么开始,然后再转向如何。
当命名空间被引入时,想法是将所有内容定义在命名空间中,以便不同的库不会相互干扰。但这引入了一个操作符问题。例如,请看下面的代码:
namespace N
{
  class X {};
  void f(X);
  X& operator++(X&);
}

int main()
{
  // define an object of type X
  N::X x;

  // apply f to it
  N::f(x);

  // apply operator++ to it
  ???
}
当然,你可以写成N::operator++(x),但这样做将会破坏重载运算符的全部意义。因此需要找到一种解决方案,使得编译器能够在不在作用域内的情况下找到operator++(X&)。另一方面,它仍然不应该找到在另一个不相关的命名空间中定义的另一个operator++,这可能会导致调用的不确定性(在这个简单的例子中,你不会遇到不确定性,但在更复杂的例子中,你可能会)。解决办法是参数相关查找(ADL),因为查找取决于参数(更确切地说,取决于参数的类型)。由于该方案是由Andrew R. Koenig发明的,因此也经常被称为Koenig查找。
诀窍在于对于函数调用,除了常规名称查找(在使用点处查找作用域中的名称)之外,还会在任何给定给函数的参数类型的作用域中进行第二次查找。因此,在上面的示例中,如果你在主函数中写入x++,它会在全局作用域中查找operator++,同时还会在定义x的类型N::X所在的作用域中查找,即在namespace N中查找。然后它找到了匹配的operator++,因此x++就可以正常工作了。然而,另一个在另一个命名空间中定义的operator++(比如N2)将不会被找到。由于ADL不限于命名空间,你也可以在main()中使用f(x)代替N::f(x)

谢谢!我从来没有真正理解它为什么存在! - user965369

22

在我看来,并不是所有关于它的东西都是好的。人们,包括编译器供应商,由于它的有时不幸的行为而对它进行了侮辱。

ADL 对 C++11 中的 for-range 循环进行了重大改革。为了理解为什么 ADL 有时会产生意外的影响,请考虑不仅考虑参数定义的命名空间,还要考虑参数模板参数的参数、函数类型/指针类型的参数类型/指针类型的参数类型等等。

一个使用 boost 的示例

std::vector<boost::shared_ptr<int>> v;
auto x = begin(v);

如果用户使用boost.range库,将会导致一个歧义性问题,因为ADL(关联依赖查找)会同时找到std::begin (使用std::vector)和boost::begin (使用boost::shared_ptr)。


我一直在想,首先考虑模板参数有什么好处。 - Dennis Zickefoose
ADL只推荐给运算符使用,对于其他函数最好显式地编写命名空间,这样说是否公平? - balki
它是否也考虑了参数基类的命名空间?(如果是的话,那当然是疯狂的)。 - Alex B
3
如何修复?使用std::begin吗? - paulm
有趣的阅读材料:https://dev59.com/EVwX5IYBdhLWcg3wtBMa#33576098 - rubenvb
这对于 Boost 来说很容易解决 - 摆脱 boost::begin 并在 namespace boost 中替换为 using std::begin。Boost 中所有已添加到 std 的内容都需要从 namespace boost 中删除,只留下 using std::whatever - Chris Dodd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接