第十章 函数模板
摘自 http://thunderbird2.itpub.net/category/5549/52292
10.1 函数模板定义
1. (与宏做比较)函数模板提供了一种机制通过它我们可以保留函数定义和函数调用的语义在一个程序位置上封装了一段代码确保在函数调用之前实参只被计算一次而无需像宏方案那样绕过C++的强类型检查;
2. (实质)函数模板提供一个种用来自动生成各种类型函数实例的算法;
3. (形式)程序员对于函数接口参数和返回类型中的全部或者部分类型进行参数化而函数体保持不变;
4. (场合)如果用一个函数的实现在一组实例上保持不变并且每个实例都处理一种惟一的数据类型,那么该函数就是模板的最佳候选者;
5. (格式)
template <typename|class p1, …., className pi,….>
(1)模板参数可以是一个模板类型参数,它代表了一种类型;
模板类型参数由关键字class 或typename 后加一个标识符构成在函数的模板参数表中,这两个关键字的意义相同它们表示后面的参数名代表一个潜在的内置或用户定义的类型模板参数名由程序员选择。
模板类型参数被用作一个类型指示符可以出现在模板定义的余下部分,它的使用方式与内置或用户定义的类型完全一样,比如用来声明变量和强制类型转换。
(2)模板参数是一个模板非类型参数,它代表了一个常量表达式。
模板非类型参数由一个普通的参数声明构成模板非类型参数表示该参数名代表了一个潜在的值而该值代表了模板定义中的一个常量
模扳非类型参数被用作一个常量值,可以出现在模板定义的余下部分,它可以用在要求常量的地方,或许是在数组声明中指定数组的大小,或作为枚举常量的初始值。
(3)函数定义或声明跟在模板参数表后,除了模板参数是类型指示符或常量值外函数模板的定义看起来与非模板函数的定义相同。
6.约束
(1)如果在全局域中声明了与模板参数同名的对象函数或类型则该全局名将被隐藏; 在函数模板定义中声明的对象或类型不能与模板参数同名(重名错误)
(2)如果一个函数模板有一个以上的模板类型参数,则每个模板类型参数前面都必须有关键字class 或typename;
在表达式前加上关键字ypename可以告诉编译器一个表达式是类型表达式.
(3) 如同非模板函数一样, 函数模板也可以被声明为inline 或extern。应该把指示符放在模板参数表后面,而不是在关键字template 前面。
10.2 函数模板实例化
1. 定义
(1) 模板实例化。函数模板指定了怎样根据一组或更多实际类型或值构造出独立的函数。这个构造过程被称为模板实例化template instantiation 。
这个过程是隐式发生的,它可以被看作是函数模板调用或取函数模板的地址的副作用。函数模板在它被调用或取其地址时被实例化。
(2)模板实参推演。参用函数实参的类型来决定模板实参的类型和值的过程被称为模板实参推演
2. 约束
在取函数模板实例的地址时,必须能够通过上下文环境为一个模板实参决定一个惟一的类型或值。如果不能决定出这个惟一的类型或值就会产生编译时刻错误。
Typedef int (*pf)(int (&)[10]) //声明函数指针
template <typename Type, int size>
Type min( Type (&r_array)[size] ) { /* ... */ }
typedef int (&rai)[10];
typedef double (&rad)[20];
void func( int (*)(rai) );
void func( double (*)(rad) );
int main() {
// 错误: 哪一个 min() 的实例?
func( &min );
}
如果我们用一个强制类型转换显式地指出实参的类型则可以消除编译时刻错误。
10.3 模板实参推演
1. (定义)当函数模板被调用时,对函数实参类型的检查决定了模板实参的类型和值,这个过程被称为模板实参推演(template argument deduction)
func(int arg[10]) arg的类型不是int[], 而是int*
在模板实参推演期间决定模板实参的类型时, 编译器不考虑函数模板实例的返回类型。
2.要想成功地进行模板实参推演,函数实参的类型不一定要严格匹配相应函数参数的类型。下列三种类型转换是允许的:左值转换、限定转换和到一个基类(该基类根据一个类模板实例化而来)的转换。
(1)左值转换包括从左值到右值的转换、从数组到指针的转换、从函数到指针的转换。
int size = sizeof (ai) / sizeof (ai[0]); //确定数组个数的方法
(2)限定修饰转换把const 或volatile 限定修饰符加到指针上
所以函数实参在模板实参被推演之前,就先被转换为const Type*型了;然后Type 的模板实参被椎演为实参的类型。
(3)基类转换
如果函数参数的类型是一个类模板,且如果实参是一个类,它有一个从被指定为函数参数的类模板实例化而来的基类,则模板实参的推演就可以进行。
3. 约束
(1)多个函数实参可以参加同一个模板实参的推演过程如果模板参数在函数参数表中出现多次,则每个推演出来的类型都必须与根据模板实参推演出来的第一个类型完全匹配。
(2)这些可能的类型转换的限制只适用于参加模板实参推演过程的函数实参。对于所有其他实参,所有的类型转换都是允许的。
4. 模板实参推演的通用算法
1 依次检查每个函数实参以确定在每个函数参数的类型中出现的模板参数
2 如果找到模板参数则通过检查函数实参的类型推演出相应的模板实参
3 函数参数类型和函数实参类型不必完全匹配.下列类型转换可以被应用在函数实参上,以便将其转换成相应的函数参数的类型:
左值转换
限定修饰转换
从派生类到基类类型的转换假定函数参数具有形式T<args> T<args>&或T<args>*
则这里的参数表args 至少含有一个模板参数
4 如果在多个函数参数中找到同一个模板参数,则从每个相应函数实参推演出的模板实参必须相同.
10.4 显式模板实参
1. 引入显式模板实参原因
(1)在某些情况下编译器不可能推演出模板实参的类型
(2)模板实参推演过程为同一模板实参推演出两个不同的类型
2.格式
模板实参被显式指定在逗号分隔的列表中,用尖括号(<> 一个小于号和一个
大于号)括起来,紧跟在函数模板实例的名字后面。
3. 效果
(1) 当函数模板实参被显式指定时,把函数实参转换成相应函数参数的类型可以应用任何隐式类型转换。
(2)除了允许在函数实参上的类型转换显式模板参数,还为解决其他的程序设计问题提供了方案。
// T1 不出现在函数模板参数表中
template <class T1, class T2, class T3>
T1 sum( T2, T3 );
在显式特化explicit specification 中,我们只需列出不能被隐式推演的模板实参,如同缺省实参一样我们只能省略尾部的实参。
// ok: T3 是 unsigned int
// T3 从 ui 的类型中推演出来
ui_type loc3 = sum< ui_type, char >( ch, ui );
// ok: T2 是 char, T3 是 unsigned int
// T2 和 T3 从 pf 的类型中推演出来
ui_type (*pf)( char, ui_type ) = &sum< ui_type >;
// 错误: 只能省略尾部的实参
ui_type loc4 = sum< ui_type, , ui_type >( ch, ui );
4. 使用指南
显式模板实参应该只被用在完全需要它们来解决二义性或在模板实参不能被推演出来的上下文中使用模板实例时。
(1)让编译器来决定模板实参的类型和值是比较容易的
(2)如果我们通过修改程序中的声明来改变在函数模板实例调用中的函数实
参的类型,则编译器会自动用不同的模板实参实例化函数模板,而无需我们做任何事情。
(3)如果我们指定了显式模板参数,则必须检查显式模板实参对于函数实参的新类型是否仍然合适。所以建议在可能的时候省略显式模板实参
10.5 模板编译模式
C++支持两种模板编译模式包含模式(Inclusion Model) 和分离模式(Separation Model)
10.5.1 包含编译模式
在包含编译模式下,我们在每个模板被实例化的文件中包含函数模板的定义,并且往往把定义放在头文件中,像对内联函数所做的那样。
缺点:
(1) 函数模板体body 描述了实现细节,对于这些细节用户可能想忽略或者我们希望隐藏起来不让用户知道。
(2) 如果函数模板的定义非常大,那么在头文件中给出的细节层次有可能是不可接受的。
(3) 在多个文件之间编译相同的函数模板定义增加了不必要的编译时间。
10.5.2 分离编译模式
在分离编译模式下,函数模板的声明被放在头文件中。在这种模式下,函数模板声明和
定义的组织方式与程序中的非内联函数的声明和定义组织方式相同。
定义的实现形式:
通过在模板定义中的关键字template 之前加上关键字export 来声明一个可导出的
函数模板。
(1) 关键字export 不需要出现在头文件的模板声明中
(2) 一个函数模板只能被定义为export 一次
分离模式使我们能够很好地将函数模板的接口同其实现分开,进而组织好程序,以便把
函数模板的接口放到头文件中,而把实现放在文本文件中.
10.6 模板显式特化