模板是一个非常强大的C++功能,STL的各种组件也是基于模板的。所以,无论是写程序了,还是读程序,都有必要了解一下C++的模板。
模板定义并不是真正的定义了一个函数或者类,而是编译器根据程序员缩写的模板和形参来自己写出一个对应版本的定义,这个过程叫做模板实例化。编译器编 成的版本通常被称为模板的实例。编译器为程序员生成对应版本的具体过程。类似宏替换。
模板类在没有调用之前是不会生成代码的。由于编译器并不会直接编译模板本身,所以模板的定义通常放在头文件中。
如下代码为 模板实例化
#include
using namespace std;
// 定义一个函数模板
template<typename T>
void TempFun(T a)
{
cout << a << endl;
}
int main()
{
TempFun(1); // 实例化为 TempFun(1)
TempFun("1"); // 实例化为 TempFun("1")
return 0;
}
在 C++ 中为了操作简洁我们引入了函数模板。所谓的函数模板实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来表达。这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个这个模板来取代模板中的虚拟类型,从而实现了不同函数的功能。
类模板是类的抽象,类是类模板的实例。
template<typename T> class DefClass {}; // 类模板
template<typename T> void DefTempParm() {} // 函数模板
函数模板在c++98中与类模板一起被引入,不过类模板支持默认模板参数,而函数模板不支持默认模板参数,不过在c++11中已经解决该限制,如下所示
void DefParm(int m = 3) {} // C++98 编译通过, C++11 编译通过
template<typename T = int> class DefClass {}; // C++98 编译通过, C++11 编译通过
template<typename T = int> void DefTempParm() {} // C++98 编译失败, C++11 编译通过
可以看到,DefTempParm函数模板拥有一个默认参数。使用仅支持C++98的编译器编译,DefTempParm的编译会失败,而支持C++11的编译器则毫无问题。不过在语法上,与类模板有些不同的是,在为多个默认模板参数声明指默认值的时候,程序员必须遵照“从右往左”的规则进行指定。而这个条件对函数模板来说并不是必须的。如下所示
template<typename T1, typename T2 = int> class DefClass1;
template<typename T1 = int, typename T2> class DefClass2; // 无法通过编译
template<typename T, int i = 0> class DefClass3;
template<int i = 0, typename T> class DefClass4; // 无法通过编译
template<typename T1 = int, typename T2> void DefFun1(T1 a, T2 b);
template<int i = 0, typename T> void DefFun2(T a);
函数模板的参数推导规则也并不复杂。简单地讲,如果能够从函数实参中推导出类型的话,那么默认模板参数就不会被使用,反之,默认模板参数则可能会被使用。我们可以看看下面这个来自于C++11标准草案的例子
template<class T, class U = double>
void f(T t = 0, U u = 0);
void g()
{
f(1, 'c'); // f(1, 'c')
f(1); // f(1, 0), 使用了默认模板参数 double
f(); // 错误: T 无法被推导出来
f<int>(); // f(0, 0), 使用了默认模板参数 double
f<int, char>(); // f(0, 0)
}
我们定义了一个函数模板f,f同时使用了默认模板参数和默认函数参数。可以看到,由于函数的模板参数可以由函数的实参推导而出,所以在{1)这个函数调用中,我们实例化出了模板函数的调用应该为f < int,double>(1,0),其中,第二个类型参数U使用了默认的模板类型参数double,而函数实参则为默认值0。类似地,{inb>0实例化出的模板函数第二参数类型为double,值为0。而表达式f由于第一类型参数T的无法推导,从而导致了编译的失败。而通过这个例子我们也可以看到,默认模板参数通常是需要跟默认函数参数一起使用的。
还有一点应该强调一下,模板函数的默认形参不是模板参数推导的依据。函数模板参数的选择,总是由函数的实参推导而来的。
“外部模板”是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这个符号的一份定义。具体地,a.o
中的i是实在存在于a.o
目标文件的数据区中的数据,而在b.o
中,只是记录了i
符号会引用其他目标文件中数据区中的名为i
的数据。这样一来,在链接器(通常由编译器代为调用)将a.o
和b.o
链接成单个可执行文件(或者库文件)c的时候,文件的数据区也只会有一个i
的数据(供a.c
和b.c
的代码共享)。
而如果b.c
中我们声明int i
的时候不加上extern
的话,那么i就会实实在在地既存在于a.o
的数据区中,也存在于b.o
的数据区中。那么链接器在链接a.o
和b.o
的时候,就会报告错误,因为无法决定相同的符号是否需要合并。
而对于函数模板来说,现在我们遇到的几乎是一模一样的问题。不同的是,发生问题的不是变量(数据),而是函数(代码)。这样的困境是由于模板的实例化带来的。
外部模板作用:对编译器的编译时间的优化,减少冗余的代码,减少开销。
比如,我们在一个test.h
的文件中声明了如下一个模板函数:
template <typedef T> void fun(T){}
在第一个testl.cpp
文件中,我们定义了以下代码:
#include "test.h"
void test1(){ fun(3); }
而在另一个test2.cpp
文件中,我们定义了以下代码:
#include "test.h"
void test1(){ fun(4); }
由于两个源代码使用的模板函数的参数类型一致,所以在编译testl.cpp
的时候,编译器实例化出了函数fun< int>(int)
,而当编译test2.cpp
的时候,编译器又再一次实例化出了函数
fun< int>(int)
。那么可以想象,在testl.o
目标文件和test2.o
目标文件中,会有两份一模一样
的函数fun< int>(int)
代码。代码重复和数据重复不同。数据重复,编译器往往无法分辨是否是要共享的数据;而代码重复,为了节省空间,保留其中之一就可以了(只要代码完全相同)。事实上,大部分链接器也是这样做的。在链接的时候,链接器通过一些编译器辅助的手段将重复的模板函数代码fun< int>(int)删除掉,只保留了单个副本。这样一来,就解决了模板实例化时产生的代码冗余问题。我们可以看看下图中的模板函数的编译与链接的过程示意。
对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要移除重复的实例化代码。很明显,这样的工作太过多余,而在广泛使用模板的项目中,由于编译器会产生大量余代码,++会极大地增加编译器的编译时间和链接时间++。解决这个问题的方法基本跟变量共享的思路是一样的,就是使用“外部的”模板。
外部模板的使用实际依赖于C++98中一个已有的特性,即显式实例化(Explicit Instantiation)。显式实例化的语法很简单,比如对于以下模板:
template <typedef T> void fun(T) {}
我们只需要声明:
template void fun<int>(int);
这就可以使编译器在本编译单元中实例化出一个fnint>(int)版本的函数(这种做法也被称为强制实例化)。而在C++11标准中,又加人了外部模板(ExtemTemplate)的声明。语法上,外部模板的声明跟显式的实例化差不多,只是多了一个关键字extern。对于上面的例子,我们可以通过:
template void fun<int>(int);
这样的语法完成一个外部模板的声明。那么回到一开始我们的例子,来修改一下我们的代码。首先,在testl.cpp做显式地实例化:
#include "test.h"
template void fun<int>(int);//显式实例化
void test1(){ fun(3); }
接下来,在test2.cpp中做外部模板的声明:
#include "test.h"
extern template void fun(int);//显式实例化
void test1(){ fun(3); }
这样一来,在test2.o中不会再生成fun< int>(in)的实例代码。整个模板的实例化流程如下图所示
可以看到,由于test2.o不再包含fun < int>(int)的实例,因此链接器的工作很轻松,基本跟外部变量的做法是一样的,即只需要保证让testl.cpp和test2.cpp共享一份代码位置即可。而同时,编译器也不用每次都产生一份fun< int>(int)的代码,所以可以减少编译时间。
这里也可以把外部模板声明放在头文件中,这样所有包含test.h的头文件就可以共享这个外部模板声明了。这一点跟使用外部变量声明是完全一致的。在使用外部模板的时候,我们还需要注意以下问题**:如果外部模板声明出现于某个编译单元中,那么与之对应的显示实例化必须出现于另一个编译单元中或者同一个编译单元的后续代码中;外部模板声明不能用于一个静态函数(即文件域函数),但可以用于类静态成员函数(这一点是显而易见的,因为静态函数没有外部链接属性,不可能在本编译单元之外出现**.
在实际上,C++11中“模板的显式实例化定义、外部模板声明和使用”好比“全局变量的定义、外和使用”方式的再次应用。++不过相比于外部变量声明,不使用外部模板声明并不会导致任何问题++。如我们在本节开始讲到的,++外部模板定义更应该算作一种针对编译器的编译时间及空间的优化手段++。很多时候,由于程序员低估了模板实例化展开的开销,因此大量的模板使用会在代码中产生大量的余。这种余,有的时候已经使得编译器和链接器力不从心。++但这并不意味着程序员需要为四五十行的代码写很多显式模板声明及外部模板声明。只有在项目比较大的情况下。我们才建议用户进行这样的优化++。
总的来说,就是在既不忽视模板实例化产生的编译及链接开销的同时,也不要过分担心模板展开的开销。
举例如下:
如下则是不使用外部模板,会在test1.o和test2.o都实例化出函数fun< int>(int)
//test.h
template <typename T>
void fun(T t){
}
//test1.cpp
void test1(){
fun<int>(1);
}
//test2.cpp
void test2(){
fun<int>(1);
如下则是使用了外部模板,则在test2.o中不会实例化出函数fun< int>(int),
//test.h
template <typename T>
void fun(T t){
}
//test1.cpp
extern template void fun<int>(int);
void test1(){
fun<int>(1);
}
//test2.cpp
void test2(){
fun<int>(1);
在C++98中,标准对模板实参的类型还有一些限制。具体地讲,局部的类型和匿名的类在C++98中都不能做模板类的实参。比如,如下列代码所示的代码在C++98中很多都无法编译通过
template<typename T> class X {};
template<typename T> void TempFun(T t) {}
struct A {} a;
struct
{
int i;
} b; // b 是匿名类型变量
typedef struct
{
int i;
} B; // B 是匿名类型
void Fun()
{
struct C {} c; // C 是局部类型
X<A> x1; // C++98 通过, C++11 通过
X<B> x2; // C++98 错误, C++11 通过 // 实际上使用 clang++ -c 2-13-1.cpp 编译通过,clang-1100.0.33.16
X<C> x3; // C++98 错误, C++11 通过
TempFun(a); // C++98 通过, C++11 通过
TempFun(b); // C++98 错误, C++11 通过
TempFun(c); // C++98 错误, C++11 通过
}
在上述代码中,我们定义了一个模板类×和一个模板函数TempFun,然后分别用普通的全局结构体、名的全局结构体,以及局部的结构体作为参数传给模板。可以看到,使用了局部的结构体C及变量c,以及名的结构体B及变量b的模板类和模板函数,在C+98标准下都无法通过编译。而除了名的结构体之外,匿名的联合体以及枚举类型,在C++98标准下也都是无法做模板的实参的。如今看来这都是不必要的限制。所以在C++11会标准允了以上类型做模板参数的做法,故而用支持C++11标准的编译器编译以上代码,所示代码可以通过编译。不过值得指出的是,虽然匿名类型可以被模板参数所接受了,但并不意味着以下写法可以被接受,如下列代码所示。
template<typename T> struct MyTemplate {};
int main()
{
MyTemplate<struct { int a; }> t; // 无法编译通过, 匿名类型的声明不能在模板实参位置
return 0;
}
在上述代码中,我们把匿名的结构体直接声明在了模板实参的位置。这种做法非常直观,但却不符合C/C++的语法。在C/C++中,即使是匿名类型的声明,也需要独立的表达式语句。要使用名结构体作为模板参数,则可如同代码一样对置名结构体作别名。此外在第4章我们还会看到使用C++11独有的类型推导decltype,也可以完成相同的功能.