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

What is "Argument-Dependent Lookup" (aka ADL, or "Koenig Lookup")?

提问人:user965369 提问时间:11/13/2011 最后编辑:Deduplicatoruser965369 更新时间:1/21/2021 访问量:41077

问:

关于什么是参数相关查找,有哪些很好的解释?许多人也称它为 Koenig Lookup。

我最好想知道:

  • 为什么这是一件好事?
  • 为什么这是一件坏事?
  • 它是如何工作的?
C++ 参数依赖查找 名称查找 C++-常见问题解答

评论

2赞 Flexo 11/13/2011
gotw.ca/gotw/030.htm
11赞 sehe 11/13/2011
这是一件好事,因为否则: std::cout << “Hello world”; 不会编译
0赞 Li Kui 3/19/2018
en.cppreference.com/w/cpp/language/adl

答:

89赞 Nawaz 11/13/2011 #1

在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赞 Nawaz 4/10/2015
@AdamBadura:请注意,这是函数的一个参数,足以启用 ADL。你注意到了吗?std::cout
1赞 Nawaz 5/27/2015
@meet:您的问题需要很长的答案,而此空间无法提供。因此,我只能建议您阅读以下主题:1)签名(如它作为参数的内容和返回的内容)。2) 完全限定名称(like 或 )。3)对参数相关查找的更详细研究。ostream<<std::vectorstd::operator<<
1赞 WorldSEnder 7/7/2015
代码有问题:应该是,见 ideone.com/FFKA7bstd::operator<<(std::operator<<(std::cout, s), std::endl);std::operator<<(std::cout, s).operator<<(std::endl);
2赞 Nawaz 7/16/2015
@WorldSEnder:是的,你是对的。可以作为参数的函数实际上是一个成员函数。无论如何,如果我使用 代替 ,那么我的答案是正确的。谢谢你的评论。std::endl"\n"std::endl
2赞 Nawaz 6/6/2016
@Destructor:因为 形式的函数调用调用了一个自由函数。所以在 的情况下,没有这样的自由函数作为第二个参数。它是作为参数的成员函数,您必须为其编写 .并且由于存在一个作为第二个参数的自由函数,因此有效; 也会起作用。f(a,b)std::operator<<(std::cout, std::endl);std::endlstd::endlstd::cout.operator<<(std::endl);char const*"\n"'\n'
283赞 Alok Save 11/13/2011 #2

Koenig Lookup,或参数相关查找,描述了编译器如何在 C++ 中查找非限定名称。

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

当函数调用 (5.2.2) 中的 postfix-expression 是 unqualified-id 时,可以搜索在通常的非限定查找 (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.
}

在上面的例子中,既没有 -declaration 也没有 -directive,但编译器仍然通过应用 Koenig 查找正确地将非限定名称标识为命名空间中声明的函数。usingusingdoSomething()MyNamespace

它是如何工作的?

该算法告诉编译器不仅要查看局部范围,还要查看包含参数类型的命名空间。因此,在上面的代码中,编译器发现作为函数参数的对象属于命名空间。因此,它会查看该命名空间以找到 的声明。objdoSomething()MyNamespacedoSomething()

Koenig 查找的优势是什么?

正如上面的简单代码示例所示,Koenig 查找为程序员提供了便利和易用性。如果没有 Koenig 查找,程序员就会产生开销,重复指定完全限定的名称,或者使用大量 -declarations。using

为什么批评 Koenig 查找?

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

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

std::swap(obj1,obj2);

可能不会表现出与以下行为相同的行为:

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

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

如果存在命名空间 ,并且 , , 和 存在,则第二个示例将导致对 的调用,这可能不是用户想要的。AA::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 lookup”?

因为它是由前AT&T和贝尔实验室研究员和程序员安德鲁·科尼希(Andrew Koenig)设计的。

延伸阅读:


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

评论

15赞 legends2k 7/28/2014
@AlokSave:答案+1,但琐事不正确。Koenig 并没有发明 ADL,正如他在这里承认的那样:)
27赞 Anthony Hall 12/10/2014
批评 Koenig 算法的例子可以被认为是 Koenig 查找的“功能”,也可以被认为是“缺点”。以这种方式使用 std::swap() 是一个常见的习惯语:如果没有提供更专业的版本 A::swap(),请提供“使用 std::swap()”。如果 A::swap() 的专用版本可用,我们通常希望调用该版本。这为 swap() 调用提供了更多的通用性,因为我们可以信任该调用来编译和工作,但我们也可以信任要使用的更专业的版本(如果有的话)。
6赞 Adam Badura 4/10/2015
@anthrond 还有更多内容。实际上,您必须这样做,因为唯一的选择是为您的类添加模板函数显式专用化。然而,如果你的类本身就是一个模板,那么它将是部分专业化,而不是显式专业化。并且不允许模板功能的部分专用化。添加重载是一种替代方法,但明确禁止(您不能向命名空间添加内容)。因此ADL 是 .std::swapstd::swapAAstd::swapstdstd::swap
2赞 Arvid 10/25/2016
我本来希望在“koenig 查找的优势”下看到提到重载运算符。这个例子似乎有点倒退。我希望问题确实出在选择时,而不是特定于类型的重载,.这个例子似乎具有误导性。因为永远不会对用户类型有特定的重载,所以我认为这不是一个很好的例子。std::swap()std::swap()A::swap()std::swap(A::MyClass&, A::MyClass&)std
1赞 6/13/2018
@gsamaras......和?我们都可以看到,该函数从未被定义过。您的错误消息证明它实际上有效,因为它正在寻找 ,而不仅仅是 .MyNamespace::doSomething::doSomething
35赞 celtschk 11/13/2011 #3

也许最好从为什么开始,然后才去怎么做。

当引入命名空间时,我们的想法是在命名空间中定义所有内容,这样单独的库就不会相互干扰。然而,这给运营商带来了一个问题。例如,请看以下代码:

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
  ???
}

当然,你可以写 ,但那样会打败运算符重载的全部意义。因此,必须找到一个解决方案,允许编译器找到它,尽管它不在范围内。另一方面,它仍然不应该在另一个不相关的命名空间中找到另一个定义,这可能会使调用变得不明确(在这个简单的示例中,你不会得到歧义,但在更复杂的示例中,你可能会)。解决方案是参数相关查找 (ADL),以这种方式调用,因为查找依赖于参数(更准确地说,取决于参数的类型)。由于该方案是由 Andrew R. Koenig 发明的,因此它通常也被称为 Koenig 查找。N::operator++(x)operator++(X&)operator++

诀窍在于,对于函数调用,除了正常的名称查找(在使用时在作用域中查找名称)之外,还会在给定给函数的任何参数类型的作用域中进行第二次查找。因此,在上面的示例中,如果您在 main 中编写,它不仅会在全局范围内查找,还会在定义 , 类型的范围内查找,即在 中。在那里它找到一个匹配的 ,因此可以工作。但是,在另一个命名空间中定义的另一个命名空间(例如 )将找不到。由于 ADL 不限于命名空间,因此也可以使用 而不是 in 。x++operator++xN::Xnamespace Noperator++x++operator++N2f(x)N::f(x)main()

评论

0赞 user965369 11/13/2011
谢谢!从来没有真正明白它为什么在那里!
22赞 Johannes Schaub - litb 11/13/2011 #4

在我看来,并非一切都是好的。人们,包括编译器供应商,一直在侮辱它,因为它有时是不幸的行为。

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

使用 boost 的示例

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

如果用户使用 boost.range 库,这会导致歧义,因为两者都被找到(由 ADL 使用 )和被找到(由 ADL 使用 )。std::beginstd::vectorboost::beginboost::shared_ptr

评论

0赞 Dennis Zickefoose 11/14/2011
我一直想知道首先考虑模板参数有什么好处。
0赞 balki 4/16/2013
是否可以公平地说,ADL 仅推荐用于运算符,而最好为其他函数显式编写命名空间?
0赞 Alex B 6/28/2013
它是否还考虑参数基类的命名空间?(当然,如果这样做,那就太疯狂了)。
3赞 paulm 12/31/2014
如何解决?使用 std::begin?
0赞 rubenvb 5/25/2018
关于该主题的有趣阅读: stackoverflow.com/a/33576098/256138