Namespaces
Variants

Argument-dependent lookup

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

实参依赖查找(ADL),又称柯尼希查找 [1] ,是一组用于在 函数调用表达式 中查找非限定函数名的规则集,包括对 重载运算符 的隐式函数调用。除了常规 非限定名称查找 所考虑的作用域和命名空间外,这些函数名还会在其参数所属的命名空间中进行查找。

实参依赖查找使得使用其他命名空间中定义的运算符成为可能。示例:

#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); // 正确:此为函数调用,ADL 检查 std 命名空间
                     // 因为 endl 的实参属于 std,从而找到 std::endl
    (endl)(std::cout); // 错误:“endl”未在此命名空间声明
                       // 子表达式 (endl) 不是非限定标识符
}

目录

详情

首先,若由常规 非限定查找 产生的查找集合包含以下任意元素,则不会进行实参依赖查找:

1) 类成员的声明。
2) 在块作用域声明的函数(非 using declaration )。
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) 若实参为一组重载函数(或函数模板)的名称或 取址表达式 ,则重载集中的每个函数都会被检查,其关联的类与命名空间集合会被加入该集合。
  • 此外,若该重载集通过 模板标识符 指定,则其所有类型模板参数与模板模板参数(非常量模板参数除外)都会被检查,其关联的类与命名空间集合也会被加入该集合。

如果关联类和命名空间集合中的任何命名空间是 内联命名空间 ,则其外围命名空间也会被加入该集合。

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

(since C++11)

在确定关联的类和命名空间集合后,该集合中类内发现的所有声明将在后续ADL处理过程中被舍弃,但如下文第2点所述,命名空间作用域的友元函数和函数模板除外。

通过常规 非限定查找 找到的声明集合,与通过ADL在关联集合的所有元素中找到的声明集合,将按照以下特殊规则进行合并:

1) using directives 在关联命名空间中被忽略。
2) 在关联类中声明的命名空间作用域友元函数(及函数模板),即使通过普通查找不可见,也能通过ADL可见。
3) 除函数和函数模板外,所有名称均被忽略(与变量无冲突)。

注释

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

ADL 是泛型代码中交换两个对象既定惯用法的原因: using std:: swap ; swap ( obj1, obj2 ) ; ,因为直接调用 std:: swap ( obj1, obj2 ) 不会考虑可能在 obj1 obj2 类型所在命名空间中定义的用户自定义 swap() 函数,而仅调用非限定版本的 swap ( obj1, obj2 ) 在未提供用户自定义重载时将无法调用任何函数。特别地, std::iter_swap 及所有其他标准库算法在处理 Swappable 类型时均采用此方法。

名称查找规则使得在全局或用户定义命名空间中声明作用于 std 命名空间类型的运算符变得不切实际,例如为 std::vector std::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); // 因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); // 正确:限定查找找到模板'f'
    N2::f<3>(s); // 错误:N2::f不接受常量参数
                 //        N1::f未被查找,因为ADL仅对非限定名称生效
    using N2::f;
    f<3>(s); // 正确:非限定查找现在找到N2::f
             //     随后ADL因该名称为非限定而生效,并找到N1::f
}
(until C++20)

在以下语境中仅发生 ADL 查找(即仅在关联命名空间中查找):

  • 当成员查找失败时,由 范围for循环 执行的非常成员函数 begin end 的查找。
(since C++11)
(since 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 通过普通非限定查找找到的块作用域函数声明
未能阻止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)