实参依赖查找

来自cppreference.com
< cpp‎ | language

实参依赖查找(argument-dependent lookup),又称 ADL 或 Koenig 查找 [1],是一组对函数调用表达式(包括对重载运算符的隐式函数调用)中的无限定的函数名进行查找的规则。在通常无限定名字查找所考虑的作用域和命名空间之外,还会在它的各个实参的命名空间中查找这些函数。

实参依赖查找使得使用在不同命名空间定义的运算符成为可能。例如:

#include <iostream>
int main()
{
    std::cout << "测试\n"; // 全局命名空间中没有 operator<<,但 ADL 检验 std 命名空间,
                           // 因为左实参在 std 命名空间中
                           // 并找到 std::operator<<(std::ostream&, const char*)
    operator<<(std::cout, "测试\n"); // 同上,用函数调用记法
 
    // 然而,
    std::cout << endl; // 错误:'endl' 未在此命名空间中声明。
                       // 这不是对 endl() 的函数调用,所以不适用 ADL
 
    endl(std::cout); // OK:这是函数调用:ADL 检验 std 命名空间,
                     // 因为 endl 的实参在 std 中,并找到了 std::endl
 
    (endl)(std::cout); // 错误:'endl' 未在此命名空间声明。
                       // 子表达式 (endl) 不是函数调用表达式
}

细节

首先,如果通常的无限定查找所生成的集合含有下列任何内容,那么不考虑实参依赖查找:

1) 类成员的声明
2) 块作用域的(非 using 声明的)函数声明
3) 任何非函数或函数模板的声明(例如函数对象或另一变量,它的名字与正在查找的函数名冲突)

否则,对于每个函数调用表达式中的实参,检验它的类型,以确定它将向查找所添加的命名空间与类的关联集

1) 对于基础类型的实参,命名空间与类的关联集是空集
2) 对于类类型(含联合体)的实参,集合由以下组成:
a) 该类自身
b) 它所有的直接与间接基类
c) 如果该类是另一类的成员,该外围类
d) 添加到集合的各个类的最内层外围命名空间
3) 对于类型是类模板的特化的实参,在上述关于类的规则外,还检验以下类型,并将与它关联的类与命名空间添加到集合中:
a) 为各类型模板形参提供的所有模板实参的类型(跳过非类型模板形参和模板模板形参)
b) 以任何模板模板实参为其中成员的命名空间
c) 以任何模板模板实参为其中成员的类(如果它们是类成员模板)
4) 对于任何枚举类型的实参,向集合中添加该枚举类型的声明的最内层外围命名空间。如果该枚举类型是类成员,那么向集合中添加该类。
5) 对于 T 的指针或指向 T 的数组的指针类型的实参,检验类型 T 并向集合中添加它的类与命名空间的关联集合。
6) 对于函数类型的实参,检验各函数形参类型与函数返回值类型,并向集合中添加它们的类与命名空间的关联集合。
7) 对于指向类 X 的成员函数 F 的指针类型的实参,检验各函数形参类型、函数返回值类型及类 X,并向集合中添加它们的类与命名空间的关联集合。
8) 对于指向类 X 的数据成员 T 的指针类型的实参,检验该成员类型和类型 X,并向集合添加它们的类与命名空间的关联集合。
9) 如果实参是一组重载函数(或函数模板)的名字或取址表达式,那么检验重载集合中的每个函数,并向集合添加它的类与命名空间的关联集合。
a) 另外,如果以模板标识(带模板实参的模板名)指名重载集,那么检验它的所有类型模板实参与模板模板实参(但不包括非类型模板实参),并向集合添加它的类与命名空间的关联集合。

如果类与命名空间的关联集合中的任何命名空间是内联命名空间,那么向集合中添加它的外围命名空间。

如果类与命名空间的关联集合中的任何命名空间直接含有内联命名空间,那么向集合中添加该内联命名空间。

(C++11 起)

在确定命名空间与类的关联集合后,为了进一步的 ADL 处理,忽略此集中所有在类中找到的声明,但不包括命名空间作用域的友元函数及函数模板,在后述点 2 陈述。

根据下列特殊规则,将通过常规无限定查找所找到的声明的集合,与通过 ADL 所生成的关联集合的所有元素中找到的声明集合进行合并:

1) 忽略关联命名空间中的 using 指令
2) 在关联类中的命名空间作用域声明的友元函数(及函数模板)通过 ADL 可见,即使它们通过普通查找不可见。
3) 忽略除函数与函数模板外的所有名字(不会与变量之间发生冲突)

注解

因为实参依赖查找,在相同命名空间定义的非成员函数和非成员运算符被认为是该类公开接口的一部分(如果它们被 ADL 找到)[2]

ADL 是在泛型代码中为交换两个对象而建立的手法能成立的原因:
using std::swap;
swap(obj1, obj2);
因为直接调用 std::swap(obj1, obj2) 不会考虑用户定义的 swap() 函数,它可能在与 obj1 或 obj2 类型之定义相同的空间定义,而仅调用无限定的 swap(obj1, obj2)会在没有用户定义重载时无法调用任何函数。特别是 std::iter_swap 与所有其他标准库算法在处理可交换 (Swappable) 类型时使用此手段。

名字查找规则使得在全局或用户定义命名空间中声明对来自 std 命名空间的类型进行操作的运算符变得不切实际,例如,对于 std::vectorstd::pair 的自定义 operator+operator>>(除非 vector/pair 的元素类型是用户定义类型,这会将它的命名空间添加到 ADL 中)。这种运算符不会从诸如标准库算法的模板实例化中被查找到。进一步细节见待决名

ADL 能找到完全在类或类模板内定义的友元函数(典型的例子是重载的运算符),即使它始终未在命名空间层次进行声明。

template<typename T>
struct number
{
    number(int);
    friend number gcd(number x, number y) { return 0; }; // 类模板内的定义
};
 
// 除非提供匹配声明,否则 gcd 是此命名空间的不可见成员(除非通过 ADL)
void g()
{
    number<double> a(3), b(4);
    a = gcd(a,b); // 找到 gcd ,因为 number<double> 是关联类,
                  // 使 gcd 在它的命名空间(全局命名空间)可见
//  b = gcd(3,4); // 错误:gcd 不可见
}

尽管普通查找在找不到结果时也能通过 ADL 解析函数调用,但是对带显式指定模板实参的函数模板调用还是要求存在普通查找所能找到的模板声明(否则,它将是遇到未知名字后随小于号的语法错误):

namespace N1
{
    struct S {};
 
    template<int X>
    void f(S);
}
 
namespace N2
{
    template<class T>
    void f(T t);
}
 
void g(N1::S s)
{
    f<3>(s);     // C++20 前是语法错误(无限定查找找不到 f)
    N1::f<3>(s); // OK,有限定查找找到模板 'f'
    N2::f<3>(s); // 错误: N2::f 不接收非类型模板形参
                 //       N1::f 不能被找到,因为 ADL 仅适用于无限定名
 
    using N2::f;
    f<3>(s); // OK:无限定查找现在找到 N2::f,
             //     然后因为此名无限定所以 ADL 表态并找到 N1::f
}
(C++20 前)

下列语境之中进行 ADL(即仅在关联的命名空间中查找):

  • 范围 for 循环,成员查找失败时,查找非成员函数 beginend
(C++11 起)
(C++17 起)

示例

来自 http://www.gotw.ca/gotw/030.htm 的示例

namespace A
{
    struct X;
    struct Y;
 
    void f(int);
    void g(X);
}
 
namespace B
{
    void f(int i)
    {
        f(i); // 调用 B::f(无限递归)
    }
 
    void g(A::X x)
    {
        g(x); // 错误:在 B::g(常规查找)与 A::g(实参依赖查找)间有歧义
    }
 
    void h(A::Y y)
    {
        h(y); // 调用 B::h(无限递归):ADL 检验命名空间 A
              // 但是找不到 A::h,所以只采用来自常规查找的 B::h
    }
}

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 33 C++98 当用于查找的实参是重载函数或函数模板的地址时,关联的命名空间和类未指明 指明它们
CWG 90 C++98 嵌套的非联合体类的类关联集不包含该类的外围类,但嵌套联合体与它的外围类关联 所有嵌套类都关联对应外围类
CWG 239 C++98 即使通常的无限定查找找到了块作用域内的函数声明,还是会进行实参依赖查找 该场合下除了找到 using 声明
外都不再考虑实参依赖查找
CWG 997 C++98 待决的形参类型和返回类型不会被添加到函数模板的命名空间与类的关联集 它们会被添加到关联集
CWG 1690 C++98
C++11
ADL 无法找到返回的 lambda(C++11)和局部类类型对象(C++98) 现在可以找到它们
CWG 1691 C++11 ADL 对不可见枚举声明的行为出人意料 已修正
CWG 1692 C++98 双重嵌套类没有关联的命名空间(它们的外围类不是命名空间成员) 关联命名空间扩展到最内层外围命名空间

参阅

外部链接

  1. Andrew Koenig: "A Personal Note About Argument-Dependent Lookup"
  2. H. Sutter (1998) "What's In a Class? - The Interface Principle" in C++ Report, 10(3)