目录
14.1 On-Demand实例化
14.2 延迟实例化
14.2.1 部分实例化和完整实例化
14.2.2 实例化组件
14.3 C++实例化模型
14.3.1 两阶段查找
14.3.2 POI
14.3.3 包含式模型
14.4 几种实现方案
14.4.1 贪婪实例化
14.4.2 查询实例化
14.4.3 迭代实例化
参考:cpp-templates-2nd/第14章 实例化.md at master · r00tk1ts/cpp-templates-2nd (github.com)
C++模板实例化的概念非常基础,但有时又错综复杂。这一复杂性的其中一个底层原因在于:模板生成的实体定义不再局限于源代码单一的位置。模板本身的位置、模板使用的位置以及模板实参定义的位置均在实体的含义中扮演着重要角色。
on-demand:请求式、按需、点播。
当C++编译器遇到模板特化的使用时,它会用需要的实参来替换模板参数来生成特化体。这一过程是自动完成的,不需要客户端代码来引导(或者不需要模板定义来引导)。这一”on-demand“实例化特性使得C++与其他早期的编译型语言的类似功能大相径庭(如Ada或Eiffel,其中的一些语言需要显式地实例化引导,另外一些使用运行时分发机制来避免编译期实例化过程)。有时这也被称作”隐式(implicit)实例化“或者”自动(automatic)实例化“。
On-demand实例化意味着编译器常常需要访问模板完整的定义(换句话说,不只是声明)以及某些成员。考虑下面这一段精简的源码文件:
template class C; // #1 declaration only
C* p = 0; // #2 fine: definition of C not needed
template
class C{
public:
void f(); // #3 member declaration
}; // #4 class template definition completed
void g(C& c) // #5 use class template declaration only
{
c.f(); // #6 use class template definition;
} // will need definition of C::f()
// in this translation unit
template
void C::f() // required definition due to #6
{
}
另一个需要类模板实例化的表达式如下所示,这里需要C
实例化是因为它需要该类型的尺寸:
C* p = new C;
本例中,需要实例化来保证编译器可以确定C
的尺寸,该new表达式需要去确认要分配多少存储空间。你可能会发现,对这一模板来说,替换模板参数T
的实参X
的类型无论是什么,都不会影响模板的尺寸,毕竟C
是一个空类(没有成员变量或虚函数)。然而,编译器并不会通过分析模板定义来避免实例化(所有编译器实际上都会进行实例化)。
在源代码中是否需要访问类模板的成员并不总是那么直观。例如,C++重载决议规则要求:如果候选函数的参数是类类型,那么该类类型就必须是可见的:
template
class C {
public:
C(int); // a constructor that can be called with a single parameter
}; // may be used for implicit conversions
void candidate(C); // #1
void candidate(int) { } // #2
int main()
{
candidate(42); // both previous function declarations can be called
}
调用candidate(42)
会采用#2
处的声明。然而,在#1
处的声明也会被实例化来检查对于这个调用来说它是否是可用的候选者(这个例子中,由于模板的单实参构造器可以把42隐式转换成一个类型为C
的右值)。请注意,如果模板不经实例化也可以找到调用函数(合适的候选),编译器还是被允许(但不强制)执行该实例化(上例的情景中,由于有精准匹配的候选者,隐式转换的那个不会被选择)。
现在有一个相关问题:模板实例化的程度如何?可以给出这样的模糊答案:会实例化到它实际需要的程度。换句话说,编译器在实例化模板时应该是“懒惰”的。
如我们之前所见,编译器有时不需要替换类或函数模板的完整定义。例如:
template T f(T p) { return 2*p; }
decltype(f(2)) x = 2;
本例中,decltype(f(2))
所指示的类型并不需要函数模板f()
的完整实例化。编译器因此只被允许替换f()
的声明,而不是替换整个“身体”。这有时被称为部分实例化(partial instantiation)。
同样,如果引用类模板的实例而不需要将该实例作为完整类型,则编译器不应对该类模板实例执行完整的实例化。考虑下面的例子:
template class Q {
using Type = typename T::Type;
};
Q* p = 0; // OK: the body of Q is not substituted
在这里,Q
完整的实例化会触发一个错误,因为在T
是int
类型时,T::Type
并没有意义。但是因为本例并不需要完整的Q
,所以不会执行完整实例化,代码也是OK的(尽管可疑)。
当类模板隐式(完整)实例化时,其所有成员的声明也都会进行实例化,但是对应的定义却并不会实例化(即,成员是部分实例化的)。对此有一些特殊情况:首先,如果类模板包含一个匿名的联合体(union),该联合体的成员的定义也会实例化;另一个特殊的情况出现在虚成员函数场景中,它们的定义作为模板实例化的结果,可能会也可能不会进行实例化。实际上,许多实现都会实例化该定义,因为“实现虚函数调用机制的内部结构”需要虚函数有一个链接实体存在。
模板实例化就是从对应的模板实体通过合适地模板参数替换来得到一个常规的类型、函数或是变量的过程。这可能听起来直截了当,但实际上需要遵循非常多的细节。
在第13章中,我们曾看到依赖型名称无法在解析模板时被找到。取而代之的是,它们会在实例化的时刻再次进行查找。非依赖型名称则会在更早的阶段被查找,因此当模板第一次看到它的时候,就可以诊断出许多错误。这就引出了“两阶段查找”的概念。第一阶段查找发生在解析模板的时候,而第二阶段查找发生在模板实例化的时候:
如上所述,C++编译器会在模板客户端代码的某些位置访问模板实体的声明或者定义。当某些代码结构引用了模板特化,而且为了生成该特化需要实例化相应的模板定义时,就会在源代码中产生一个POI。POI是源代码中的一个点,在这里会插入已被替换的模板。例如:
class MyInt {
public:
MyInt(int i);
};
MyInt operator - (MyInt const&);
bool operator > (MyInt const&, MyInt const&);
using Int = MyInt;
template
void f(T i)
{
if(i > 0) {
g(-i);
}
}
// #1
void g(Int)
{
// #2
f(42); // point of call
// #3
}
// #4
C++编译器看到f
时,它知道模板f
需要用MyInt
替换T
来实例化:这就产生了一个POI。#2
和#3
与该调用点紧邻,但是它们都不适合做POI,因为C++不允许我们在这里插入::f
的定义。此外,#1
和#4
两处的本质区别在于,在#4
处,函数g(Int)
是可见的,因此模板依赖的调用g(-i)
可以在#4
处被解析。然而,如果我们假定#1
是POI的话,那么调用g(-i)
将不能被解析,因为g(Int)
在#1
处是不可见的。幸运的是,对于函数模板特化的引用,C++把它的POI定义,置于紧跟在“包含这个引用的定义或声明所在的最近的命名空间作用域”之后。在我们的例子中,这个位置就是#4
。
变量模板POI的处理与函数模板相似。而对于类模板特化来说,情况则不太一样,如下例所示:
template
class S {
public:
T m;
};
// #1
unsigned long h()
{
// #2
return (unsigned long)sizeof(S);
// #3
}
// #4
老规矩,#2
和#3
都不能作为POI,这两个位置不能进行命名空间作用域类S
的定义(模板是不能出现在函数作用域内部的)。假如我们可以遵循函数模板实例的规则,POI将会出现在位置#4
处,然而,这样一来,表达式sizeof(S
是无效的,这是因为S
的尺寸直到#4
之后才能被确定。因此,生成的类模板实例的引用被紧邻地定义在包含该引用的声明或定义的命名空间作用域之前。在我们的例子中,这个位置就是#1
。
当遇到POI时,对应模板的定义必须是可访问的。对类特化来说,这意味着类模板定义必须在编译单元中被更早地看见。而对函数模板和变量模板(以及类模板的成员函数和静态数据成员)的POI来说,也同样需要。典型的模板定义被简单的通过#include
语句引入到编译单元,尽管是非类型模板也一样。这种模板定义的源码模型被称为包含式模型,它目前是当下C++标准所支持的模板的唯一自动源码模型。
贪婪实例化假定链接器会意识到特定的实体(尤其是可链接的模板实例化体),它们大多在多个目标文件和库中重复出现。编译器会以一种特殊的方式标记这些实体。当链接器发现了多个实例时,它会保留单个并丢弃掉所有其他的。这就是贪婪实例化的处理方法。
在这一方案中,程序中参与的所有编译单元会汇集一个共享的数据库。该数据库可以追溯哪些特化体被实例化了,并且可以找到其所依赖的源代码。生成的特化体本身会把信息存储在数据库中。当可链接实体遇到一个POI时,会进入下面的处理流程:
Cfront的迭代过程如下所述:
第3步中,这种迭代的要求基于这样的事实:在实例化一个可链接实体过程中,可能会要求”另一个仍未实例化“的实体进行实例化;最后,所有的迭代都已经完成,链接器才会成功创建一个完整的程序。