2.12 外部模板
类别:部分人
2.12.1 为什么需要外部模板
“外部模板”是C++11中一个关于模板性能上的改进。实际上,“外部”(extern)这个概念早在C的时候已经就有了。通常情况下,我们在一个文件中a.c中定义了一个变量int i,而在另外一个文件b.c中想使用它,这个时候我们就会在没有定义变量i的b.c文件中做一个外部变量的声明。比如:
extern int i;
这样做的好处是,在分别编译了a.c和b.c之后,其生成的目标文件a.o和b.o中只有i这个符号[1]的一份定义。具体地,a.o中的i是实在存在于a.o目标文件的数据区中的数据,而在b.o中,只是记录了i符号会引用其他目标文件中数据区中的名为i的数据。这样一来,在链接器(通常由编译器代为调用)将a.o和b.o链接成单个可执行文件(或者库文件)c的时候,c文件的数据区也只会有一个i的数据(供a.c和b.c的代码共享)。
而如果b.c中我们声明int i的时候不加上extern的话,那么i就会实实在在地既存在于a.o的数据区中,也存在于b.o的数据区中。那么链接器在链接a.o和b.o的时候,就会报告错误,因为无法决定相同的符号是否需要合并。
而对于函数模板来说,现在我们遇到的几乎是一模一样的问题。不同的是,发生问题的不是变量(数据),而是函数(代码)。这样的困境是由于模板的实例化带来的。
注意 这里我们以函数模板为例,因为其只涉及代码,讲解起来比较直观。如果是类模板,则有可能涉及数据,不过其原理都是类似的。
比如,我们在一个test.h的文件中声明了如下一个模板函数:
template<typename T>void fun(T){}
在第一个test1.cpp文件中,我们定义了以下代码:
include "test.h"
void test1(){fun(3);}
而在另一个test2.cpp文件中,我们定义了以下代码:
include "test.h"
void test2(){fun(4);}
由于两个源代码使用的模板函数的参数类型一致,所以在编译test1.cpp的时候,编译器实例化出了函数fun<int>(int),而当编译test2.cpp的时候,编译器又再一次实例化出了函数fun<int>(int)。那么可以想象,在test1.o目标文件和test2.o目标文件中,会有两份一模一样的函数fun<int>(int)代码。
代码重复和数据重复不同。数据重复,编译器往往无法分辨是否是要共享的数据;而代码重复,为了节省空间,保留其中之一就可以了(只要代码完全相同)。事实上,大部分链接器也是这样做的。在链接的时候,链接器通过一些编译器辅助的手段将重复的模板函数代码fun<int>(int)删除掉,只保留了单个副本。这样一来,就解决了模板实例化时产生的代码冗余问题。我们可以看看图2-1中的模板函数的编译与链接的过程示意。
图 2-1 模板函数的编译与链接
不过读者也注意到了,对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要移除重复的实例化代码。很明显,这样的工作太过冗余,而在广泛使用模板的项目中,由于编译器会产生大量冗余代码,会极大地增加编译器的编译时间和链接时间。解决这个问题的方法基本跟变量共享的思路是一样的,就是使用“外部的”模板。
[1]符号(symbol)是编译器/链接器的术语,读者可以简单地将它想象为一个变量名字。