实参依赖查找

< 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) 另外,若以 模板标识(带模板实参的模板名)指名重载集,则检验其所有类型模板实参与模板模板实参(但不包括非类型模板实参),并向集合添加其类与命名空间的关联集合。

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

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

在确定命名空间与类的关联集合后,为了进一步的 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);      // 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
    }
}


参阅

引用

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