编译的过程为编译原理所涉及的内容,不做展开。
主要关注:
- 模板在阶段7,8进行的两阶段查找
- 模板为何要放到头文件中:包含编译模式(inclusion model)
- 最常见的编译器实现:贪婪具现化(greedy instantiation)
模板
两阶段查找
模板具现化是通过正确地替换模板中的模板参数,来获得类型、函数或变量的过程。C++标准规定模板的具现化过程为两阶段查找(two-phase lookup):
第一阶段:解析模板
- 在 非待决名 上使用 有限定的名字查找 或 无限定的名字查找。
- 在 无限定的待决名 上使用无限定的名字查找,确认其是否是一个模板。然而我们在第一阶段不进行ADL。ADL被推迟到第二阶段。我们认为此次查找的结果暂时是不完整的。
- 此时无法处理有限定的待决名。
第二阶段:模板实例化
- 在实例化点(POI: point of instantiation)对模板进行实例化。
- 对 有限定的待决名 进行 有限定的名字查找
- 对 无限定的待决名 进行 ADL 将结果与第一阶段进行的 无限定的名字查找 的结果合并。
例子一:为什么需要两阶段查找?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
29template<typename T>
class Trap
{
public:
enum { x }; // #1
};
template<typename T>
class Victim
{
public:
int y;
void poof()
{
Trap<T>::x * y; // #2
}
};
template<>
class Trap<void>
{
public:
using x = int; // #3
};
void boom(Victim<void>& bomb)
{
bomb.poof();
}
在主模板的#1
处,x
的类型为一个enum
。在全特化模板的#3
处,x
是一个int
的alias。因此在#2
处的有限定的待决名Trap<T>::X
在Trap<T>
未实例化时无法被决议。如果T
是一个void
,则该语句生命了一个int*
,如果T
是其他类型,则该语句为一个乘法运算。在#2
的只能确定该标识符是一个有限定的待决名,其他都不可知。只有等到Trap<T>
实例化以后,我们才能对其进行决议。因此两阶段查找的必要的。第二阶段就发生在实例化以后。
例子二:
1 | template<typename T> |
在第一阶段#1
位置的g1(x)
,因为依赖于x
的类型,因此是一个无限定的待决名。此时我们对其进行 无限定的名字查找,由于g1(int)
在其下方,因此 无限定的名字查找 查找结果为空。第二阶段在#2
处,POI后进行ADL。由于参数类型为int
,因此生成的类和命名空间的集合都为空,因此对g1
的查找失败。
实例化点(point of instantiation)
当代码中涉及到某一模板的特化时,该模板必须被具现化以产生该特化。因此编译器必须在该涉指附近安插代码以使得该特话被具现化。安插代码的位置就被称作实例化点。
标准规定:
The POI for a reference to a function template specialization is immediately after the nearest namespace scope declaration or definition that contains that reference.
The POI for a reference to a class template specialization is immediately before the nearest namespace scope declaration or definition that contains that reference.
1 | class MyInt |
为了实例化//point of call
位置的函数模板f<Int>(int)
,我们需要在其附近插入::f<int>(int);
作为实例化点。位置#2
和#3
在函数内部,C++中不可以在函数内部直接插入另一个函数的声明。位置#1
和#4
的区别在于在#1
处,void g(Int)
还不可见。如果在#1
处插入POI,则无法解析// call g
处对g
调用。因此实例化点被安排在#4
。#4
作为实例化点也是符合标准的,因为包含//point of call
的最近的命名空间作用域为函数void g(int)
的命名空间作用域,其immediately after
的位置刚好是#4
。