命名空间
变体
动作

实参依赖查找

出自 cppreference.cn
< cpp‎ | language
 
 
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) 不会考虑用户定义的 swap() 函数,这些函数可能与 obj1obj2 的类型在同一命名空间中定义,并且如果没有提供用户定义的重载,则仅调用非限定的 swap(obj1, obj2) 将不会调用任何内容。 特别是,std::iter_swap 和所有其他标准库算法在处理 Swappable 类型时都使用这种方法。

名称查找规则使得在全局或用户定义的命名空间中声明对来自 std 命名空间的类型进行操作的运算符(例如,operator>>operator+ 用于 std::vectorstd::pair)变得不切实际(除非 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 查找(即,仅在关联的命名空间中查找):

  • 如果成员查找失败,则由范围 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++ 标准。

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

[编辑] 参见

[编辑] 外部链接

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