命名空间
变体
操作

实参依赖查找

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
 
 

实参依赖查找(ADL),也称为 Koenig 查找[1],是查找函数调用表达式中不限定函数名称的规则集,包括对重载运算符的隐式函数调用。这些函数名称除了通常不限定名称查找所考虑的作用域和命名空间外,还会在其实参的命名空间中查找。

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

#include <iostream>
 
int main()
{
    std::cout << "Test\n"; // There is no operator<< in global namespace, but ADL
                           // examines std namespace because the left argument is in
                           // std and finds std::operator<<(std::ostream&, const char*)
    operator<<(std::cout, "Test\n"); // Same, using function call notation
 
    // However,
    std::cout << endl; // Error: “endl” is not declared in this namespace.
                       // This is not a function call to endl(), so ADL does not apply
 
    endl(std::cout); // OK: this is a function call: ADL examines std namespace
                     // because the argument of endl is in std, and finds std::endl
 
    (endl)(std::cout); // Error: “endl” is not declared in this namespace.
                       // The sub-expression (endl) is not an unqualified-id
}

目录

[编辑] 详情

首先,如果通常的不限定查找产生的查找集包含以下任何一项,则不考虑实参依赖查找:

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) 如果实参是一组重载函数(或函数模板)的名称或取地址表达式,则检查重载集中的每个函数,并将其关联的类和命名空间集添加到集合中。
  • 此外,如果重载集由模板标识符命名,则检查其所有类型模板实参和模板模板实参(但非类型模板实参除外),并将其关联的类和命名空间集添加到集合中。

如果关联的类和命名空间集中的任何命名空间是内联命名空间,则其封闭命名空间也添加到集合中。

如果关联的类和命名空间集中的任何命名空间直接包含内联命名空间,则该内联命名空间添加到集合中。

(C++11 起)

在确定关联的类和命名空间集之后,为了进一步的 ADL 处理,该集合中的类中找到的所有声明都被丢弃,除了下面第 2 点所述的命名空间作用域友元函数和函数模板。

普通不限定查找找到的声明集与 ADL 产生的关联集所有元素中找到的声明集合并,并遵循以下特殊规则:

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

[编辑] 注意

由于实参依赖查找,与类在同一命名空间中定义的非成员函数和非成员运算符被视为该类的公共接口的一部分(如果通过 ADL 找到它们)[2]

ADL 是在泛型代码中交换两个对象的既定惯用语背后的原因:using std::swap; swap(obj1, obj2);因为直接调用std::swap(obj1, obj2)不会考虑可能在obj1obj2类型所在命名空间中定义的用户定义swap()函数,而仅调用不限定的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; }; // Definition within
                                                         // a class template
};
 
// Unless a matching declaration is provided gcd is
// an invisible (except through ADL) member of this namespace
void g()
{
    number<double> a(3), b(4);
    a = gcd(a, b); // Finds gcd because number<double> is an associated class,
                   // making gcd visible in its namespace (global scope)
//  b = gcd(3, 4); // Error; gcd is not visible
}

尽管即使普通查找找不到任何内容,函数调用也可以通过 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);     // Syntax error until C++20 (unqualified lookup finds no f)
    N1::f<3>(s); // OK, qualified lookup finds the template 'f'
    N2::f<3>(s); // Error: N2::f does not take a non-type parameter
                 //        N1::f is not looked up because ADL only works
                 //              with unqualified names
 
    using N2::f;
    f<3>(s); // OK: Unqualified lookup now finds N2::f
             //     then ADL kicks in because this name is unqualified
             //     and finds N1::f
}
(C++20 前)

在以下上下文中,会发生仅 ADL 查找(即,仅在关联命名空间中查找):

  • range-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); // Calls B::f (endless recursion)
    }
 
    void g(A::X x)
    {
        g(x); // Error: ambiguous between B::g (ordinary lookup)
              //        and A::g (argument-dependent lookup)
    }
 
    void h(A::Y y)
    {
        h(y); // Calls B::h (endless recursion): ADL examines the A namespace
              // but finds no A::h, so only B::h from ordinary lookup is used
    }
}

[编辑] 缺陷报告

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

缺陷报告 应用于 发布时的行为 正确的行为
CWG 33 C++98 关联的命名空间或类未指定
如果用于查找的实参是
一组重载函数或函数模板的地址
已指定
CWG 90 C++98 嵌套非联合类的关联类
不包括其封闭类,但嵌套
联合与其封闭类相关联
非联合也关联
CWG 239 C++98 在普通不限定查找中找到的块作用域函数声明
并没有阻止 ADL 发生
using 声明外,
不考虑 ADL
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 双重嵌套类没有关联的命名空间
(它们的封闭类不是任何命名空间的成员)
关联命名空间
扩展到最内层
封闭命名空间
CWG 2857 C++98 不完整类类型的关联类
包括其基类
不包括

[编辑] 另请参阅

[编辑] 外部链接

  1. Andrew Koenig:《关于实参依赖查找的个人说明
  2. H. Sutter (1998) 《类中有什么?——接口原则》载于 C++ Report, 10(3)