参数依赖查找

来自cppreference.com
< cpp‎ | language

参数依赖查找(Argument-dependent lookup),又称 ADL 或 Koenig 查找,是一组于函数调用表达式查找非限定函数名的规则,包含对重载运算符的隐式函数调用。在通常非限定名称查找所考虑的作用域和命名空间之外,还在其参数的命名空间中查找这些函数。

参数依赖查找使使用定义于不同命名空间的运算符可行。例如:

#include <iostream>
int main()
{
    std::cout << "Test\n"; // 全局命名空间无 operator<< ,但 ADL 检验 std 命名空间,
                           // 因为左参数在 std 命名空间中
                           // 并找到 std::operator<<(std::ostream&, const char*)
    operator<<(std::cout, "Test\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) 另外,若重载集为模板 id (带模板实参的模板名)所指名,则检验其所有类型模板实参与模板模板实参(但无非类型模板实参),并添加其类与命名空间的关联集到集合。

若类与命名空间的关联集中的任何命名空间是内联命名空间,则添加其外围命名空间到集合。

若类与命名空间的关联集中的任何命名空间直接含有内联命名空间,则添加该内联命名空间到集合。

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

以下列特殊规则,合并普通非限定查找找到的声明集合,与在 ADL 所生成关联集的所有元素中找到的声明集合

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

[编辑] 注意

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

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);      // 语法错误(无限定查找找不到 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
}

下列语境发生仅 ADL 的查找(即仅于关联的命名空间查找):

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


[编辑] 参阅

[编辑] 引用

  1. H. Sutter (1998) "What's In a Class? - The Interface Principle" in C++ Report, 10(3)