名称的分类

分类 说明
标识符(identifier) 由字母、数字和下划线组成,不能以数字开头
运算符函数id 关键字operator后跟运算符的符号,比如operator newoperator[]
转换函数id 表示用户定义的隐式转换运算符,比如operator int&
字面量运算符id 表示用户定义的字面量运算符,比如运算符""_km,在编写像100_km这样的字面量时可以使用
模板id 模板的名称,后面紧跟着一对角括号标识的模板参数,比如List<T, int, 0>;也可以是运算符函数id或者字面量运算符id后面跟着一对角括号标识的模板参数,比如operator+<X<int>>
非受限id 上述都是非受限id,还包括析构函数,是广义上的标识符
受限id 需要说明调用范围的id,如使用类、枚举类型或命名空间的名称对非受限的id进行限定,或者使用全局作用域解析运算符进行限定。比如::X::Array<T>::y这种
受限名称 经过受限查找的名称,比如S::x,或者class->mem
非受限名称 受限名称相对的名称,也就是不需要经过受限查找时的名称
名称 上述受限名称和非受限名称的集合
依赖型名称 依赖于模板参数的名称,比如显式地包含了模板参数的名称X<int>,或者使用成员访问运算符(.或->)限定的受限名称,其左侧的表达式是泛型类型,那么它也是依赖型名称。最后是依赖参数的名称查找,比如x+y中,如果参数表达式是泛型类型,那么+就是依赖型名称,需要依赖参数的类型来决定具体的实现
非依赖型名称 与依赖型名称相对的名称

其中,受限名称和依赖型名称对于理解C++的模板问题是非常有帮助的。

  • 如果一个名称前有作用域运算符或者成员访问运算符来表示所属的作用域,那么它就是受限名称,比如this->count是一个受限名称,但单独的count不是。
  • 如果一个名称以某种方式依赖于模板参数,那么它就是依赖型名称。比如T是一个模板参数,那么std::vector<T>::iterator是一个依赖型名称,而如果T是明确的类型,std::vector<int>::iterator就不是一个依赖型名称。

名称查找

C++中的名称查找还是比较复杂的,尤其是在模板的加持下(?),不过通常来说,名称查找是符合人的直觉的。

受限名称查找

受限名称的查找总会在受限名称所在的作用域中进行查找,如果找不到了就会报错。在写代码时,如果明确知道想要查找的名称在哪个作用域下,使用受限名称是比较好的编程习惯,因为这既规范了编译器的查找范围,也有助于代码的阅读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int x;

class Base {
public:
int i;
};

class Drived : public Base {

};

void f(Drived* pd) {
pd->i = 1; // 查找Base::i
Drived::x = 2; // 错误,在Drived作用域中找不到x
}

非受限名称查找

非受限名称也被称为普通查找,因为没有限定查找范围,其会在连续的封闭作用域中进行查找(比如在一个成员函数定义中,会先在这个类和基类的作用域中查找,找不到再扩大到其他作用域中查找)。

1
2
3
4
5
6
7
8
9
extern int count;   // #1

int lookup(int count) { // #2
if(count < 0) {
int count = 1; // #3
lookup(count); // 非受限count指涉#3
}
return count + ::count; // 第一个非受限count指涉#2,第二个受限count指涉#1
}

对于非受限名称查找,还有一种比较特殊的查找机制——ADL(argument-dependent lookup),依赖于参数的查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename T>
T max(T a, T b) {
return a < b ? b : a;
}

namespace bigmath {
class BigNumber {
public:
bool operator<(const BigNumber&, const BisNumber&);
//...
};
}

using bigmath::BigNumber;

bool g(const BigNumber& a, const BigNumber& b) {
return ::max(a, b);
}

问题在于,模板函数max()并不知道BigMath的命名空间,如果没有一些特殊的辅助规则,这肯定是行不通的,而且会大大限制模板的应用。这也正是ADL查找机制出现的原因。

依赖于参数的查找

ADL主要应用在函数的非受限名称查找,如果名称后面是用圆括号标识的实参表达式列表,那么ADL将继续在与调用实参类型“关联”的命名空间和类中查找这个名称。

关联命名空间和关联类的集合可以这样理解:

  • 基本类型的这个集合是空集。
  • 指针和数组类型,该集合是所引用类型的关联命名空间和关联类。
  • 枚举类型,关联命名空间是其声明所在的命名空间。
  • 类成员,外部类是关联类。
  • 类类型,关联类包括该类型本身、外部类、所有直接或间接基类;关联命名空间是关联类声明所在的命名空间;如果这个类是一个类模板的实例,还要包括模板类型实参的类型、声明模板的模板参数所在的类和命名空间。
  • 函数类型,包括所有参数的类型和返回类型的关联命名空间和关联类。
  • 类X的成员指针类型,除了成员相关的关联命名空间和关联类外,还包括类X相关的关联命名空间和关联类。

ADL会在以上对应类型的所有关联命名空间和关联类中查找该名称,例外的情况是会忽略using指令打开的命名空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;

namespace X {
void f(int) {
cout << "X::f(int)\n";
}
}

namespace N {
using namespace X;
enum E { e1 };

void f(E) {
std::cout << "N::f(E)\n";
}
}

void f(int) {
std::cout << "::f(int)\n";
}

int main() {
::f(N::e1); // 受限名称
f(N::e1); // 非受限名称,普通查找会找到::f(),ADL会找到N::f(),会调用后者
(f)(N::e1); // 不会使用ADL
return 0;
}

打印输出:

1
2
3
::f(int)
N::f(E)
::f(int)

main函数中的第一个调用是受限名称,那么毫无疑问会调用全局作用域下的f()函数,第二个调用从结果可以看到是使用到了ADL查找机制,在实参N::e1的关联命名空间中又找到了更符合的名称,于是调用的是命名空间N下的f()
正如前面提到的,ADL会忽略using打开的命名空间,所以尽管在命名空间N中打开了命名空间X,也不会查找到X中的f()
另外还有一点,如果名称被括号标识,也不会使用ADL。

依赖于参数的友元声明查找

1
2
3
4
5
6
7
8
9
10
template<typename T>
class C {
friend void f();
friend void f(const C<T>&);
};

void g(C<int>* p) {
f(); // 错误
f(*p); // 正确
}

尽管两个函数都声明为了类C的友元,但在函数g中第一个调用是非法的,因为它没有任何参数,也就没有关联命名空间和关联类。而第二个调用确实有关联类C<int>。

注入的类名称

如果在类自身的作用域中使用该类的名称,则可以把该名称称为注入的类名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

int C;

class C {
public:
static int f() {
return sizeof(C);
}
private:
int i[2];
};

int f() {
return sizeof(C);
}

int main() {
C::f();
f();
}

main函数中第一个f()调用返回类型C的大小,第二个f()调用返回变量C的大小。在类型C的成员函数中返回自身的大小,就使用到了类名称注入。

类模板也可以有注入的类名称,但稍有不同:
类模板的注入名称后面可以紧跟着模板实参,此时称为注入的类模板名称。
也可以不跟随模板实参,则表示类。如果上下文需要类型,它代表将会使用参数作为类的实参;如果上下文需要的是模板,则为模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<template<typename> class TT> class X {

};

template<typename T> class C {
C* a; // 等价于C<T>* a
C<void>& b; // 类模板名称
X<C> c; // 此时的注入名称表示的是模板C
X<::C> d; // ::C不是注入的类名称,总是表示模板

void f() {
cout << sizeof(C) << endl; // 表示类C,使用实例化的参数作为实参
}
};

类或类模板的注入类名称实际上是所定义类型的别名。对于非模板类型来说是显而易见的,因为类本身是作用域中具有该名称的唯一类型;但类模板就比较有意思了,因为这意味着注入的类名称引用了类模板的相同实例化,而不是该类模板的其他特化类型。

1
2
3
4
5
6
7
template<typename T>
class Node {
using Type = T;
Node* next; // 指涉当前的实例化
Node<Type>* previous; // 同上,指涉当前的实例化
Node<T*> parent; // 指涉未知的特化
};