时间:2023-02-06 10:30:10 | 栏目:C代码 | 点击:次
在代码中实际使用模板类构造对象或者调用模板函数时,编译器会根据调用者传给模板的实参进行模板类型推导然后对模板进行实例化,此过程中的实例化即是隐式实例化。
template<typename T> T add(T t1, T2) { return t1 + t2; } template<typename T> class Dylan { public: T m_data; }; int main() { int ret = add(3,4);//隐式实例化,int add<int>(int t1, int t2); Dylan<double> dylan;//隐式实例化 }
extern template int add<int>(int t1, int t2);//显式实例化声明 extern template class Dylan<int>; //显式实例化声明 template int add<int>(int t1, int t2); //显式实例化定义 template class Dylan<int>; //显式实例化定义
当编译器遇到显式实例化声明时,表示实例化定义在程序的其他地方(相对于当前cpp文件)即在其他某一个cpp文件中定义,因此不再按照模板进行类型推导去生成隐式实例化定义。
当编译器遇到显式实例化定义时,根据定义所提供的模板实参去实例化模板,生成针对该模板实参的实例化定义。
模板类、函数通常定义在头文件中,这些头文件会被很多cpp文件包含,在这些cpp文件中会多次使用这些模板,比如下面的例子:
//template.hpp template<typename T> class Dylan { public: T m_data; }; //test1.cpp #include "template.hpp" Dylan<int> t1; Dylan<int> t2; //test2.cpp #include "template.hpp" Dylan<int> t3; Dylan<int> t4;
在test1.cpp/test2.cpp 中多次实例化了Dylan<int>类,按说编译完后的可执行程序中会包含多份Dylan<T>的定义,然而实际上,整个程序中却只有一份Dylan<T>的定义。这个处理是在编译和链接过程中实现的,目前主流的实现模式有两种:
a. Borland模式
Borland模式通过在编译器中加入与公共块等效的代码来解决模板实例化问题。在编译时,每个文件独立编译,遇到模板或者模板的实例化都不加选择地直接编译。在链接的时候将所有目标文件中的模板定义和实例化都收集起来,根据需要只保留一个。这种方法实现简单,但因为模板代码被重复编译,增加了编译时间。在这种模式下,我们编写代码应该尽量让模板的所有定义都放入头文件中,以确保模板能够被顺利地实例化。要支持此模式,编译器厂商必须更换支持此模式的链接器。
b. Cfront模式
AT&T编译器支持此模式,每个文件编译时,如果遇到模板定义和实例化都不直接编译,而是将其存储在模板存储库中(template repository)。模板存储库是一个自动维护的存储模板实例的地方。在链接时,链接器再根据实际需要编译出模板的实例化代码。这种方法效率高,但实现复杂。在这种模式下,我们应该尽量将非内联成员模板的定义分离到一个单独的文件中,进行单独编译。
在一个链接器支持Borland模式的编译目标(编译后的可执行文件)上,g++使用Borland模式解决实例化问题。比如ELF(Linux/GNU), Mac OS X, Microsoft windows, 否则,g++不支持上述两种模式。
如何避免Borland模式的缺点?
上面我们说g++实现的是Borland 模式,由于我们为每一份实例化生成代码,这样在大型程序中就有可能包含很多重复的实例化定义代码,虽然链接阶段,链接器会剔除这些重复的定义,但仍然会导致编译过程中的目标文件(或者共享库文件)过于庞大。这时候,我们就可以通过C++11的模板显式实例化的方法解决。看下面的代码:
// template.hpp template<typename T> class Dylan { public: Dylan(T t); T m_data; }; // template.cpp #include "template.hpp" template<typename T> Dylan<T>::Dylan(T t) { m_data = t; } template class Dylan<int>; //模板实例化定义 // main.cpp #include "template.hpp" extern template class Dylan<int>; //模板实例化声明,告诉编译器,此实例化在其他文件中定义 //不需要根据模板定义生成实例化代码 int main() { Dylan<int> dylan(3);//OK, 使用在template.cpp中的定义 Dylan<float> dylan1(3.0);//error, 引发未定义错误 }
上面的代码中,我们将模板类的具体定义放在template.cpp中,并且在template.cpp中通过显式实例化定义语句具体化了Dylan<int>。在main.cpp中,我们通过显式实例化声明告诉编译器,Dylan<int>将在其他文件中定义,不需要在本文件中根据template.hpp的类模板实例化Dylan<int>。
由于我们没有针对Dylan<float>做显式实例化的声明和定义,因此Dylan<float> dylan(3.0)会根据template.hpp中的类模板定义进行隐式实例化,然而构造函数是在template.cpp文件中定义的,在template.hpp中找不到构造函数的定义,因而报错。如果把构造函数的定义挪回template.hpp,那Dylan<float>就能通过编译了。
Note:在编译中,如果指定-fno-implicit-templates,编译器就会禁止隐式实例化,从而只使用显式实例化。
参考文献:Template Instantiation (Using the GNU Compiler Collection (GCC))