无限定的名字查找

< cpp‎ | language

无限定名,即并非出现在作用域解析运算符 :: 右边的名字,其名字查找按下文所述检查各个作用域,直到找到任何种类的至少一个声明时就停止查找,即不再继续检查别的作用域。(注意:在某些语境中所进行的查找会忽略掉一些声明,例如,对用在 :: 左边的名字的查找将忽略函数、变量和枚举项的声明,而对用作基类说明符的名字的查找则会忽略所有的非类型的声明。)

为了进行无限定的名字查找,来自 using 指令所指名的命名空间中的所有声明,都被当成如同处于同时直接或间接包含这条 using 指令和所指名的命名空间的最内层的外围命名空间之中。

对函数调用运算符左边所使用的名字(等价地也包括表达式中的运算符)所进行的无限定的名字查找,在实参依赖查找中说明。

文件作用域

对于在全局(顶层命名空间)作用域中,在任何函数、类或用户声明的命名空间之外所使用的名字,检查全局作用域中这次名字的使用之前的部分:

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

命名空间作用域

对于在用户声明的命名空间中,在任何函数或类之外所使用的名字,在这个命名空间中这次名字的使用之前的部分中查找,然后再在包含这个命名空间的命名空间中这个命名空间的声明之前的部分中查找,直到到达全局命名空间。

int n = 1; // 声明
namespace N {
  int m = 2;
  namespace Y {
    int x = n; // OK,查找 ::n
    int y = m; // OK,查找 ::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; // 找到第三个(如果第二个没找到)
    }
    int i=4; // 找到第四个(如果第三个没找到)
}
 
int i=5; // 找到第五个(如果第四个没找到)
 
void A::N::f() {
    int i = 2; // 找到第二个(如果第一个没找到)
    while(true) {
       int i = 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; // 找到了第三个(但之后会被访问检查所拒绝)
    };
}
// const int i = 5; // 找到了第五个
namespace N {
    // const int i = 4; // 找到了第四个
    class Y : public M::B {
        // static const int i = 2; // 找到了第二个
        class X {
            // static const int i = 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; // 找到第三个
};
namespace M {
    // int i; // 找到第五个
    namespace N {
        // int i; // 找到第四个
        class X : public B {
            // int i; // 找到第二个
            void f();
            // int i; // 也找到第二个
        };
        // int i; // 找到第四个
    }
}
// int i; // 找到第六个
void M::N::X::f()
{
    // int i; // 找到第一个
    i = 16;
    // int i; // 找不到这个
}
namespace M {
  namespace N {
    // int i; // 找不到这个
  }
}
无论哪种方式,当检查派生类的基类时,需要遵守下列规则,它们有时被称为虚继承中的优先性
当子对象 A 是子对象 B 的基类子对象时,子对象 B 中找到的成员的名字将隐藏掉子对象 A 中相同的成员名。(但要注意这并不会隐藏继承晶格中并非 B 的基类的任何其他的 A 的非虚副本。这条规则仅当虚继承时才有效。)由 using 声明所引入的名字,被当成是包含这个声明的类中的名字来处理。检查了各个基类之后,其结果集合必须要么包含来自同一个类型的子对象的静态成员的声明,要么包含来自同一个子对象的非静态成员的声明。 (C++11 前)
构造一个查找集合,它由一些声明和在其中找到了这些声明的子对象所构成。将 using 声明替换为它们所代表的成员,将类型的声明(包括注入类名)替换为它们所代表的类型。若 C 为在其作用域中使用了这个名字的类,则首先检查 C。若 C 中的声明列表为空,则对其各个直接基类 Bi 各自构造其查找集合(当 Bi 自身也有基类时,递归地应用这条规则)。构造完成后,将各直接基类的查找集合根据以下规则合并为 C 的查找集合:
  • Bi 中的声明集合为空,则其被丢弃
  • 若迄今所构建的 C 的查找集合仍为空,则将其替换为 Bi 的查找集合
  • 对于 Bi 的查找集合中的各个子对象,若它们都是已经加入到 C 的查找集合中的至少一个子对象的基类子对象,则丢弃 Bi 的查找集合
  • 若已经加入到 C 的查找集合中的每个子对象均为 Bi 的查找集合中至少一个子对象的基类子对象,则丢弃 C 的查找集合,将之替换为 Bi 的查找集合
  • 否则,若 BiC 中的声明集合不同,则合并的结果有歧义:C 的新查找集合包含一个无效声明,和之前合并入 C 中的各子对象和由 Bi 所引入的子对象的并集。这个无效的查找集合如果在之后被丢弃了,那么它并不会导致错误。
  • 否则,C 的新查找集合具有之前合并入 C 和由 Bi 所引入的共同的声明集合和子对象的并集
(C++11 起)
struct X { void f(); };
struct B1: virtual X { void f(); };
struct B2: virtual X {};
struct D : B1, B2 {
    void foo() {
        X::f(); // OK,调用了 X::f(有限定查找)
        f(); // OK,调用了 B1::f(无限定查找)
// C++98 规则:B1::f 隐藏 X::f,因此即便从 D 通过 B2 可以访问到 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 中的 X::f,因为已经加入到 C 中的每个子对象(就是 X)
//  都是新集合(B1)中的至少一个子对象的基类,
//  其最终结果是一样的:C 的查找集合只包含在 B1 中找到的 B1::f)
    }
};
如果无限定的名字查找找到了 B 的静态成员,B 的嵌套类型,在 B 中声明的枚举项,则即便在所检查的类的继承树中有多个 B 类型的非虚基类子对象,它也是无歧义的:
struct V { int v; };
struct A {
        int a;
        static int s;
        enum { e };
};
struct B : A, virtual V { };
struct C : A, virtual V { };
struct D : B, C { };
 
void f(D& pd) {
        ++pd.v; // OK:只有一个 v,因为只有一个虚基类子对象
        ++pd.s; // OK:只有一个静态的 A::s,即便在 B 和 C 中都找到了它
        int i = pd.e; // OK:只有一个枚举符 A::e,即便在 B 和 C 中都找到了它
        ++pd.a; // 错误,有歧义:B 中的 A::a 和 C 中的 A::a
}

友元函数的定义

对于在授予友元关系的类体之中的友元函数的定义中所使用的名字,无限定的名字查找按照与成员函数相同的方式进行。对于定义于类体之外的友元函数中所使用的名字,无限定的名字查找按照与命名空间中的函数相同的方式进行。

int i = 3; // f1 找到的第三个,f2 找到的第二个
struct X {
    static const int i = 2; // f1 找到的第二个,f2 找不到这个
    friend void f1(int x)
    {
        // int i; // 找到第一个
        i = x; // 找到并改动 X::i
    }
    friend int f2();
    // static const int i = 2; // f1 在类作用域中的任何地方找到第二个
};
void f2(int x) {
    // int i; // 找到第一个
    i = x; // 找到并改动 ::i
}

友元函数的声明

对于在使来自其他类的成员函数为友元的友元函数声明的声明符中所使用的名字,如果该名字不是任何模板实参的一部分,则无限定的查找首先检查成员函数所在类的整个作用域。如果未能在这个作用域中找到(或者如果这个名字是模板实参的一部分),则继续以如同对授予友元关系的类的成员函数进行查找的方式继续查找。

// 这个类的成员函数被作为友元
struct A { 
    typedef int AT;
    void f1(AT);
    void f2(float);
    template <class T> void f3();
};
 
// 这个类授予友元关系
struct B {
    typedef char AT;
    typedef float BT;
    friend void A::f1(AT); // 对 AT 的查找找到的是 A::AT
    friend void A::f2(BT); // 对 BT 的查找找到的是 B::BT 
    friend void A::f3<AT>(); // 对 AT 的查找找到的是 B::AT 
};

默认实参

对于在函数声明的默认实参中所使用的名字,或者在构造函数的成员初始化器表达式 部分所使用的名字,在检查其外围的块、类或命名空间作用域之前,首先会找到函数形参的名字:

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 块的 catch 子句

对于在函数 try 块的 catch 子句中所使用的名字,其查找按照如同对在函数体的最外层块的最开始处使用的名字一样进行(特别是,函数形参是可见的,但这个最外层块中声明的名字则不可见)。

int n = 3; // 找到第三个
int f(int n = 2) // 找到第二个
try {
   int n = -1;  // 找不到这个
} catch(...) {
   // int n = 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; // OK:成员查找找到了 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++11 standard (ISO/IEC 14882:2011):
  • 3.4 Name lookup [basic.lookup]
  • 10.2 Member name lookup [class.member.lookup]
  • 14.6 Name resolution [temp.res]
  • C++98 standard (ISO/IEC 14882:1998):
  • 3.4 Name lookup [basic.lookup]
  • 10.2 Member name lookup [class.member.lookup]
  • 14.6 Name resolution [temp.res]

参阅