5.7 模板编译模型
读者可能已经注意到,所有列举的模板例子都是将完整定义的模板放在每个编译单元中。(例如,将它们完全放在单文件程序中,或者放在多文件程序的头文件中)。这种方法与传统的编程方法背道而驰,传统的编程方法通过将函数声明放在靠后的头文件中,而将函数实现放在独立的文件中(即,.cpp)的方法,使得普通函数的定义与它们的声明相分离。
与这种传统方法分离的理由如下:
·头文件中的非内联函数体会导致多函数的定义,从而导致链接错误。
·隐藏来自客户有益的函数实现,从而减少了编译时连接。
·商家可能将预编译代码(为一个特定的编译器编写)分配到各个头文件中,从而使得用户看不到函数的具体实现。
·头文件越小,编译时间就越短。
5.7.1 包含模型
另一方面,模板本质上不是代码,而是产生代码的指令。只有模板的实例化才是真正的代码。当一个编译器在编译期间已经看到了一个完整的模板定义,又在同一个翻译单元内碰到了这个模板实例化点的时候,它就必须涉及这样一个事实:一个相同的实例化点可能会呈现在另一个翻译单元内。处理这种情况最普遍的方法,是在每一个翻译单元内都为这个实例化生成代码,让连接器清除这些副本。另一种特殊的方法也可以很好地处理这种情况,就是用不能被内联的内联函数和虚函数表,这也是为什么虚函数表如此流行的原因之一。但是,有一些编译器宁愿依靠更复杂的机制也不愿意多次产生同一个特定的实例化。C++翻译系统也有责任避免这种由于多个相同的实例化点而产生的错误。
这种方法的一个缺点是,所有的模板源代码对客户都是可见的,因此对于销售库的商家而言,几乎没有机会隐藏他们的实现策略。包含模型的另一个缺点是,头文件比函数体分开编译时大多了。这种方式相比传统编译模型而言,大大增加了编译时间。
为了帮助减少包含模型所需要的大的头文件,C++提供了两个(不惟一的)可供选择的代码组织机制:可以使用显式实例化(explicit instantiation)来手工地实例化每一个模板特化,也可以使用导出模板(exported templates),它支持最大限度的独立的编译。
5.7.2 显式实例化
编程人员可以用手工指引编译器实例化他所选择的任何模板特化。当使用这个技术时,对于每个这样的特化,必须有且仅有一条相应的指令;否则可能会收到一个多定义的错误,就像在普通的非内联函数中使用了相同的标识符一样。为了进行示例说明,首先(错误地)将本章前面的例子中的min()模板的声明与定义相分离,遵循普通的非内联函数的标准模式。下面的例子包含了5个文件:
·OurMin. h:包含min()函数模板的声明。
·OurMin. cpp:包含min()函数模板的定义。
·UseMin1. cpp:尝试使用min()的一个int型实例化。
·UseMin2. cpp:尝试使用min()的一个double型实例化。
·MinMain. cpp:调用usemin1()和usemin2()。
当尝试建立这个程序时,连接器报告有未解析的min<int>()和min<double>()的外部引用。原因是当编译器在UseMin1和UseMin2中碰到对min()特化的调用时,只有min()的声明是可见的。由于它的定义不可用,编译器认为它可能来源于某些其他的翻译单元,这样一来在这一点上所需的特化就没有被实例化,从而将问题留给了连接器,连接器最终解释它无法找到它们。
为了解决程序中的这个问题,将引入一个新文件MinInstances.cpp,它显式地实例化了所需的min()特化:
为了手工实例化一个特定的模板特化,可以在该特化的声明前使用template关键字。注意,在这里必须包含OurMin.cpp而不是OurMin.h,这是因为编译器需要用模板定义来进行实例化。然而,这里也是程序中惟一放置该模板定义的地方[1],因为它提供了程序所需要的独一无二的min()的实例化—在其他的文件中只要有其声明就足够了。由于使用了宏预处理器来包含OurMin.cpp,因而需要加入包含警告:
现在,当把所有的文件一起编译为一个完整的程序时,就会找到min()的惟一的实例,程序就可以正确运行,输出结果如下:
编程人员也可以手工实例化类和静态数据成员。当显式实例化一个类时,除了一些之前可能已经显式实例化了的成员外,特化所需要的所有成员函数都要进行实例化。这一点很重要,因为使用这种机制时,它必须舍弃很多无用的模板——某些特定的模板将依据它们的参数类型实现不同的功能。隐式实例化在此处有优势:其中只有被调用的成员函数才进行实例化。
显式实例化多用于大型软件工程项目中,因为这样可以避免大量的编译时间。采用隐式实例化还是采用显式实例化完全独立于使用哪一个模板进行编译。可以通过使用包含模型或者分离模型(下节讨论)中的任何一种模型来进行手工实例化。
5.7.3 分离模型
模板编译的分离模型跨越了所有的翻译单元,将函数模板定义或者静态数据成员定义从它们的声明中分离出来,就像使用导出(exporting)模板机制下的普通函数和数据一样。在学习了前两节的内容后,这种说法似乎听起来有点儿奇怪。读者首先就可能会问:如果包含模型使用得很顺手,为什么还要怀疑它呢?原因有两个:有历史原因也有技术原因。
从历史上看,包含模型是第1个经历广泛的商品化使用的模型—所有的C++编译器都支持包含模型。其中的部分原因是,在进行标准化的过程中直到该过程后期也没有能够很好地说明分离模型,再一个原因就是由于包含模型本身更容易实现一些。在分离模型的语义定下来之前的很长一段时间里,就已经存在很多正在运行着的与之相关的代码了。
分离模型实现起来是如此的困难,以至于直到2003年夏天,仅有一个编译器前端(EDG)支持分离模型。那时,这个编译器如有请求,它仍旧要求模板源代码在编译时可以用来执行实例化操作。方法是在适当的位置使用一些中介代码,来取代总是要求最初的源代码随时准备好以备使用的形式。这样就可以在不需要传递源代码的情况下传递某些“预编译”模板。鉴于本章前面介绍过的查找的复杂性(就是有关在模板定义的语境中查找关联名称的内容),当编译一个实例化某个模板的程序时,仍然要以某种形式来使用一个完整模板的定义。
将一个模板定义的源代码与它的声明相分离的程序语法是很简单的。只要使用export关键字就可以了:
类似于inline或者virtual,关键字export在一个编译流中仅需出现一次。在这个编译流中,引入了一个导出模板。由于这个原因,我们在实现文件中无需重复它,但是再对它进行一下声明是一个好习惯。
之前用过的UseMin文件只需包含正确的头文件(OurMin2.h),主程序不用改动。尽管看起来这已经产生了正确的分离,但带有模板定义的文件(OurMin2.cpp)仍然必须传递给用户(因为min()的每一个实例化都必须进行处理),直至遇到这样的情况:表示模板定义的某种中介代码形式得到支持。因此,当一个正确的分离模型标准提出来的时候,其中所有的好处并不会都在今天马上体现出来。当今只有一个编译器家族支持export(那些基于EDG前端的编译器),而且这些编译器当前并没有开发将模板定义分配到已编译的格式中的潜力。
[1]正如前面解释的那样,在每一个程序中只能一次显式实例化一个模板。