Namespaces
Variants

Unqualified name 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

对于 非限定 名称,即未出现在作用域解析运算符 :: 右侧的名称,名称查找会按以下方式检查 作用域 ,直到找到至少一个任意类型的声明,此时查找停止且不再检查其他作用域。(注意:某些上下文中的查找会跳过部分声明,例如:用于 :: 左侧的名称查找会忽略函数、变量和枚举声明,用作基类说明符的名称查找会忽略所有非类型声明)。

出于非限定名称查找的目的,由 using 指令 所指定的命名空间中的所有声明,会表现得如同它们被声明在最近的、直接或间接包含该 using 指令与被指定命名空间的封闭命名空间中。

函数调用运算符左侧名称(以及表达式中运算符的等价形式)的非限定名称查找规则在 实参依赖查找 中描述。

目录

文件作用域

对于在全局(顶层命名空间)作用域使用的名称,位于任何函数、类或用户声明命名空间之外的情况,将检查使用该名称前的全局作用域:

int n = 1;     // n的声明
int x = n + 1; // 正确:查找找到 ::n
int z = y - 1; // 错误:查找失败
int y = 2;     // y的声明

命名空间作用域

对于在任意函数或类外部用户声明命名空间中使用的名称,首先搜索该名称使用处所在的命名空间,然后搜索该命名空间声明处的外围命名空间,依此类推直至抵达全局命名空间。

int n = 1; // 声明
namespace N
{
    int m = 2;
    namespace Y
    {
        int x = n; // 正确:查找找到 ::n
        int y = m; // 正确:查找找到 ::N::m
        int z = k; // 错误:查找失败
    }
    int k = 3;
}

命名空间外部的定义

对于在命名空间外部定义命名空间成员变量时使用的名称,查找方式与在命名空间内部使用名称时相同:

namespace X
{
    extern int x; // 声明而非定义
    int n = 1;    // 首先找到
}
int n = 2;        // 其次找到
int X::x = n;     // 找到 X::n,将 X::x 设为 1

非成员函数定义

对于在函数定义中使用的名称(无论是在函数体内还是作为默认参数的一部分),当该函数是用户声明或全局命名空间的成员时,名称使用所在的块会在使用该名称之前被搜索,然后在该块开始之前搜索其外围块,依此类推,直至到达作为函数体的块。随后搜索函数声明所在的命名空间,直至到达使用该名称的函数定义(不一定是声明),然后搜索外围命名空间,依此类推。

namespace A
{
    namespace N
    {
        void f();
        int i = 3; // 第3个被找到(如果第2个不存在)
    }
    int i = 4;     // 第4个被找到(如果第3个不存在)
}
int i = 5;         // 第5个被找到(如果第4个不存在)
void A::N::f()
{
    int i = 2;     // 第2个被找到(如果第1个不存在)
    while (true)
    {
       int i = 1;  // 第1个被找到:查找完成
       std::cout << i;
    }
}
// int i;          // 未找到
namespace A
{
    namespace N
    {
        // int i;  // 未找到
    }
}

类定义

对于在 类定义 中任何位置使用的名称(包括基类说明符和嵌套类定义),除了在成员函数体内部、成员函数的默认参数、成员函数的异常规范或默认成员初始化器中(其中该成员可能属于其定义位于外围类体中的嵌套类),将搜索以下作用域:

a) 该名称被使用的类体内,直至使用点为止,
b) 其基类的完整定义体,当未找到声明时递归深入它们的基类,
c) 若此类为 嵌套类 ,则包含其外围类的类体(直至此类定义处)以及外围类所有基类的完整类体,
d) 若此类为 局部类 ,或嵌套于局部类内,则包含从类定义处直至其定义点的块作用域,
e) 若此类是命名空间的成员,或是作为命名空间成员的类的嵌套类,或是作为命名空间成员的函数中的局部类,则将在命名空间作用域内进行查找,直至找到该类的定义、外围类或函数的定义;查找将继续向外层命名空间进行,直至全局作用域。

对于 友元 声明,用于确定其是否引用先前声明实体的查找过程与上述方式相同,但会在最内层封闭命名空间处停止查找。

namespace M
{
    // const int i = 1; // 永远不会被找到
    class B
    {
        // static const int i = 3;     // 第3个被找到(但无法通过访问检查)
    };
}
// const int i = 5;                    // 第5个被找到
namespace N
{
    // const int i = 4;                // 第4个被找到
    class Y : public M::B
    {
        // static const int i = 2;     // 第2个被找到
        class X
        {
            // static const int i = 1; // 第1个被找到
            int a[i]; // i 的使用
            // static const int i = 1; // 永远不会被找到
        };
        // static const int i = 2;     // 永远不会被找到
    };
    // const int i = 4;                // 永远不会被找到
}
// const int i = 5;                    // 永远不会被找到

注入类名

在类或类模板的定义内部,或从其派生的类中,当使用该类或类模板的名称时,非限定名称查找会找到正在定义的类,如同该名称是通过成员声明(具有公开成员访问权限)引入的。更多细节请参阅 注入类名

成员函数定义

对于在成员函数体内使用的名称、成员函数的默认参数、成员函数的异常规范或默认成员初始化器中使用的名称,所搜索的作用域与 类定义 中的规则相同,不同之处在于会考虑整个类作用域,而不仅仅是使用该名称的声明之前的部分。对于嵌套类,会搜索其外围类的整个主体。

class B
{
    // int i;         // 第3处找到
};
namespace M
{
    // int i;         // 第5处找到
    namespace N
    {
        // int i;     // 第4处找到
        class X : public B
        {
            // int i; // 第2处找到
            void f();
            // int i; // 同样第2处找到
        };
        // int i;     // 第4处找到
    }
}
// int i;             // 第6处找到
void M::N::X::f()
{
    // int i;         // 第1处找到
    i = 16;
    // int i;         // 始终找不到
}
namespace M
{
    namespace N
    {
        // int i;     // 始终找不到
    }
}
无论哪种方式,在检查类的继承基类时,都应遵循以下规则(有时称为 虚继承中的主导原则 ):
若子对象 B 中的成员名称与任何子对象 A 中的同名成员冲突,且 A B 的基类子对象,则前者会隐藏后者。(注意这不会隐藏继承层次中非 B 基类的其他非虚 A 副本中的名称:此规则仅对虚继承产生影响)。通过using声明引入的名称被视为包含该声明的类中的名称。检查每个基类后,最终结果集必须包含来自同类型子对象的静态成员声明,或来自同一子对象的非静态成员声明。 (until C++11)
将构建一个 查找集 ,其中包含声明及其所在的子对象。using声明会被其代表的成员替换,类型声明(包括注入类名)会被其代表的类型替换。若 C 是使用该名称的作用域所属的类,则首先检查 C 。若 C 的声明列表为空,则为其每个直接基类 Bi 构建查找集(若 Bi 自有基类则递归应用这些规则)。构建完成后,直接基类的查找集按以下方式合并到 C 的查找集中:
  • Bi 中的声明集为空,则将其丢弃,
  • 若当前构建的 C 查找集为空,则用 Bi 的查找集替换,
  • Bi 查找集中的每个子对象都是已加入 C 查找集的至少一个子对象的基类,则丢弃 Bi 的查找集,
  • 若已加入 C 查找集的每个子对象都是 Bi 查找集中至少一个子对象的基类,则丢弃 C 的查找集并替换为 Bi 的查找集,
  • 否则,若 Bi C 中的声明集不同,则结果为歧义合并: C 的新查找集包含无效声明以及先前合并到 C 与从 Bi 引入的子对象的并集。此无效查找集若在后续被丢弃可能不会报错,
  • 否则, C 的新查找集包含共享声明集以及先前合并到 C 与从 Bi 引入的子对象的并集。
(since C++11)
struct X { void f(); };
struct B1: virtual X { void f(); };
struct B2: virtual X {};
struct D : B1, B2
{
    void foo()
    {
        X::f(); // 正确:调用 X::f(限定查找)
        f(); // 正确:调用 B1::f(非限定查找)
    }
};
// C++98 规则:B1::f 会隐藏 X::f,因此即使通过 B2 可以从 D 访问到 X::f,
// 通过 D 进行的名称查找也无法找到它。
// C++11 规则:在 D 中对 f 的查找集为空,继续在基类中查找
//  在 B1 中对 f 的查找集找到 B1::f,查找完成
// 合并操作替换空集,现在 C 中对 f 的查找集包含在 B1 中找到的 B1::f
//  在 B2 中对 f 的查找集为空,继续在基类中查找
//    在 X 中对 f 的查找找到 X::f
//  合并操作替换空集,现在 B2 中对 f 的查找集包含在 X 中找到的 X::f
// 合并到 C 时发现,B2 查找集中的每个子对象(X)都是已合并子对象(B1)的基类
// 因此 B2 的查找集被丢弃
// C 最终仅保留在 B1 中找到的 B1::f
// (如果使用 struct D : B2, B1,最后一次合并将*替换* C 当前已合并的 X::f,
//  因为已添加到 C 的每个子对象(即 X)都是新集合(B1)中至少一个子对象的基类,
//  最终结果相同:C 中的查找集仅包含在 B1 中找到的 B1::f)
即使在被检查类的继承树中存在多个类型为 B 的非虚基类子对象,查找 B 的静态成员、嵌套类型以及在 B 中声明的枚举项的无限定名称查找仍然是明确的:
struct V { int v; };
struct B
{
    int a;
    static int s;
    enum { e };
};
struct B1 : B, virtual V {};
struct B2 : B, virtual V {};
struct D : B1, B2 {};
void f(D& pd)
{
    ++pd.v;       // 正确:只有一个v,因为只有一个虚基类子对象
    ++pd.s;       // 正确:只有一个静态成员B::s,即使在B1和B2中都能找到
    int i = pd.e; // 正确:只有一个枚举值B::e,即使在B1和B2中都能找到
    ++pd.a;       // 错误,存在歧义:B1中的B::a和B2中的B::a
}

友元函数定义

对于在授予友元的类体内定义的 友元 函数中使用的名称,非限定名称查找的方式与成员函数相同。对于在类体外定义的 友元 函数中使用的名称,非限定名称查找的方式与命名空间中的函数相同。

int i = 3;                     // 对f1找到第3个,对f2找到第2个
struct X
{
    static const int i = 2;    // 对f1找到第2个,对f2始终找不到
    friend void f1(int x)
    {
        // int i;              // 找到第1个
        i = x;                 // 找到并修改 X::i
    }
    friend int f2();
    // static const int i = 2; // 在类作用域内任意位置对f1找到第2个
};
void f2(int x)
{
    // int i;                  // 找到第1个
    i = x;                     // 找到并修改 ::i
}

友元函数声明

对于在 友元 函数声明中使用的名称,若该函数声明是另一个类的成员函数的友元,且该名称不属于 声明符 标识符中的任何模板参数,则非限定查找首先检查该成员函数类的完整作用域。若在该作用域中未找到(或该名称属于声明符标识符中的模板参数),则继续查找,如同查找授予友元关系的类的成员函数一样。

template<class T>
struct S;
// 其成员函数被声明为友元的类
struct A
{ 
    typedef int AT;
    void f1(AT);
    void f2(float);
    template<class T>
    void f3();
    void f4(S<AT>);
};
// 为 f1、f2 和 f3 授予友元权限的类
struct B
{
    typedef char AT;
    typedef float BT;
    friend void A::f1(AT);    // 对 AT 的查找找到 A::AT(在 A 中找到 AT)
    friend void A::f2(BT);    // 对 BT 的查找找到 B::BT(在 A 中未找到 BT)
    friend void A::f3<AT>();  // 对 AT 的查找找到 B::AT(不在 A 中查找,
                              //     因为 AT 位于声明符标识符 A::f3<AT> 中)
};
// 为 f4 授予友元权限的类模板
template<class AT>
struct C
{
    friend void A::f4(S<AT>); // 对 AT 的查找找到 A::AT
                              //(AT 不在声明符标识符 A::f4 中)
};

默认参数

对于在函数声明的 默认参数 中使用的名称,或在构造函数的 成员初始化列表 表达式 部分中使用的名称,函数参数名称会首先被查找,然后才会检查外围的块作用域、类作用域或命名空间作用域:

class X
{
    int a, b, i, j;
public:
    const int& r;
    X(int i): r(a),      // 将 X::r 初始化为引用 X::a
              b(i),      // 将 X::b 初始化为参数 i 的值
              i(i),      // 将 X::i 初始化为参数 i 的值
              j(this->i) // 将 X::j 初始化为 X::i 的值
    {}
};
int a;
int f(int a, int b = a); // 错误:查找 a 时找到的是参数 a 而非 ::a
                         // 且不允许将参数用作默认实参

静态数据成员定义

对于在 静态数据成员 定义中使用的名称,其查找方式与在成员函数定义中使用的名称相同。

struct X
{
    static int x;
    static const int n = 1; // 首先找到
};
int n = 2;                  // 其次找到
int X::x = n;               // 找到 X::n,将 X::x 设为 1 而非 2

枚举项声明

对于在 枚举声明 的初始化器部分使用的名称,首先会查找同一枚举中先前声明的枚举项,然后非限定名称查找才会继续检查外围的块作用域、类作用域或命名空间作用域。

const int RED = 7;
enum class color
{
    RED,
    GREEN = RED + 2, // RED 查找 color::RED,而非 ::RED,因此 GREEN = 2
    BLUE = ::RED + 4 // 限定查找找到 ::RED,BLUE = 11
};

函数 try 块的处理器

对于在 异常处理块 中使用的名称(属于 函数 try 的情况),名称查找的过程与在函数体最外层块起始处使用的名称相同(具体而言,函数参数可见,但在该最外层块中声明的名称不可见)

int n = 3;          // 找到第3个
int f(int n = 2)    // 找到第2个
try
{
    int n = -1;     // 从未找到
}
catch(...)
{
    // int n = 1;   // 找到第1个
    assert(n == 2); // 查找n时找到函数参数f
    throw;
}

重载运算符

对于在表达式中使用的 运算符 (例如在 a + b 中使用的 operator + ),其查找规则与显式函数调用表达式(如 operator + ( a, b ) )中使用的运算符略有不同:在解析表达式时,会执行两次独立的查找:针对非成员运算符重载和成员运算符重载(适用于允许两种形式的运算符)。随后这些集合会与内置运算符重载以平等地位合并,如 重载决议 中所述。如果使用显式函数调用语法,则执行常规的非限定名称查找:

struct A {};
void operator+(A, A);  // 用户定义的非成员 operator+
struct B
{
    void operator+(B); // 用户定义的成员 operator+
    void f();
};
A a;
void B::f() // B 成员函数的定义
{
    operator+(a, a); // 错误:从成员函数进行的常规名称查找
                     // 会在 B 的作用域中找到 operator+ 的声明
                     // 并在此停止,永远不会到达全局作用域
    a + a; // 正确:成员查找找到 B::operator+,非成员查找
           // 找到 ::operator+(A, A),重载决议选择 ::operator+(A, A)
}

模板定义

对于模板定义中使用的 非依赖名称 ,非限定名称查找在模板定义被解析时进行。此时绑定的声明不受实例化点可见声明的影响。对于模板定义中使用的 依赖名称 ,查找会推迟到模板参数已知时进行,此时 ADL 会检查 具有外部链接 (C++11 前) 且在模板定义上下文和模板实例化上下文中均可见的函数声明,而非ADL查找仅检查 具有外部链接 (C++11 前) 且在模板定义上下文中可见的函数声明(换言之,在模板定义后添加新函数声明不会使其可见,除非通过ADL)。如果ADL查找所检查的命名空间中存在具有外部链接的更优匹配(声明于其他翻译单元),或者如果检查这些翻译单元会导致查找歧义,则行为未定义。在任何情况下,如果基类依赖于模板参数,其作用域不会被非限定名称查找检查(无论是在定义点还是实例化点)。

void f(char); // f 的首次声明
template<class T> 
void g(T t)
{
    f(1);    // 非依赖名称:查找找到 ::f(char) 并立即绑定
    f(T(1)); // 依赖名称:查找推迟
    f(t);    // 依赖名称:查找推迟
//  dd++;    // 非依赖名称:查找未找到声明
}
enum E { e };
void f(E);   // f 的第二次声明
void f(int); // f 的第三次声明
double dd;
void h()
{
    g(e);  // 实例化 g<E>,此时
           // 对名称 'f' 的第二次和第三次使用进行查找
           // 找到 ::f(char)(通过常规查找)和 ::f(E)(通过 ADL)
           // 重载决议选择 ::f(E)
           // 这里调用 f(char) 一次,然后调用 f(E) 两次
    g(32); // 实例化 g<int>,此时
           // 对名称 'f' 的第二次和第三次使用进行查找
           // 仅找到 ::f(char)
           // 重载决议选择 ::f(char)
           // 这里调用 f(char) 三次
}
typedef double A;
template<class T>
class B
{
    typedef int A;
};
template<class T>
struct X : B<T>
{
    A a; // 对 A 的查找找到 ::A (double),而非 B<T>::A
};

注意:关于此规则的原理和影响,请参阅 依赖名称查找规则

模板名称

类模板外部成员的模板外查找

缺陷报告

以下行为变更缺陷报告被追溯应用于先前发布的C++标准。

缺陷报告 适用标准 发布时行为 正确行为
CWG 490 C++98 友元成员函数声明中模板实参内的
所有名称都不会在成员函数
所属类的作用域中进行查找
仅排除声明符标识符中
模板实参内的名称
CWG 514 C++98 任何在命名空间作用域使用的非限定名称
都会首先在该作用域内进行查找
用于在命名空间外部定义该命名空间
变量成员的非限定名称首先在
该命名空间内进行查找

参考文献

  • C++23 标准 (ISO/IEC 14882:2024):
  • 6.5 名称查找 [basic.lookup] (页码: 44-45)
  • 6.5.2 成员名称查找 [class.member.lookup] (页码: 45-47)
  • 13.8 名称解析 [temp.res] (页码: 399-403)
  • C++20 标准 (ISO/IEC 14882:2020):
  • 6.5 名称查找 [basic.lookup] (页码: 38-50)
  • 11.8 成员名称查找 [class.member.lookup] (页码: 283-285)
  • 13.8 名称解析 [temp.res] (页码: 385-400)
  • C++17 标准 (ISO/IEC 14882:2017):
  • 6.4 名称查找 [basic.lookup] (页码: 50-63)
  • 13.2 成员名称查找 [class.member.lookup] (页码: 259-262)
  • 17.6 名称解析 [temp.res] (页码: 375-378)
  • C++14 标准 (ISO/IEC 14882:2014):
  • 3.4 名称查找 [basic.lookup] (页码: 42-56)
  • 10.2 成员名称查找 [class.member.lookup] (页码: 233-236)
  • 14.6 名称解析 [temp.res] (页码: 346-359)
  • C++11 标准 (ISO/IEC 14882:2011):
  • 3.4 名称查找 [basic.lookup]
  • 10.2 成员名称查找 [class.member.lookup]
  • 14.6 名称解析 [temp.res]
  • C++98 标准 (ISO/IEC 14882:1998):
  • 3.4 名称查找 [basic.lookup]
  • 10.2 成员名称查找 [class.member.lookup]
  • 14.6 名称解析 [temp.res]

参见