Unqualified name lookup
对于
非限定
名称,即未出现在作用域解析运算符
::
右侧的名称,名称查找会按以下方式检查
作用域
,直到找到至少一个任意类型的声明,此时查找停止且不再检查其他作用域。(注意:某些上下文中的查找会跳过部分声明,例如:用于
::
左侧的名称查找会忽略函数、变量和枚举声明,用作基类说明符的名称查找会忽略所有非类型声明)。
出于非限定名称查找的目的,由 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; // 未找到 } }
类定义
对于在 类定义 中任何位置使用的名称(包括基类说明符和嵌套类定义),除了在成员函数体内部、成员函数的默认参数、成员函数的异常规范或默认成员初始化器中(其中该成员可能属于其定义位于外围类体中的嵌套类),将搜索以下作用域:
对于 友元 声明,用于确定其是否引用先前声明实体的查找过程与上述方式相同,但会在最内层封闭命名空间处停止查找。
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
的查找集中:
|
(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]