10 函数模板
10.1 函数模板定义
template <typename A, typename B, int size> A Func(const B(&rArray)[size]) { return A();}
>关键字template后面是用逗号分隔的模板参数表template parameter list, 用<>包括起来; 模板参数列表不能为空;(特化时可以);
模板参数可以为模板类型参数template type parameter,代表一种类型; 也可以是模板非类型参数template nontype parameter, 代表一个常量表达式;
一般情况尖括号<>中的typename代表C++内置数据类型, class代表C++数据类型(自定义);
template <class Type, int size>
Type min(const Type(&rArray)[size]) //SIZE作为参数一部分, 编译器会检查实参长度是否匹配
{
Type minVal = rArray[0];
for(int i=0; i<size; i++)
{
if(rArray[i]<minVal)
minVal = rArray[i];
}
return minVal;
}
//---
const int size = 5;
int iArray[size] = {5, 7, 3, 22, 99};
cout<<min(iArray)<<endl;
>类型和值的替换过程称为模板实例化template instantiation;
模板类型参数被当作一个类型标识符, 使用方式和内置或用户定义类型一样; e.g.声明变量, 强制类型转换;
模板非类型参数被用作常量值; e.g.数组大小, 枚举常量初值;
Note 如果在全局域中声明了与模板参数同名的对象,,函数或类型, 则该全局名将被隐藏;
函数模板定义中声明的对象或类型不能与模板参数同名;(无效或error);
模板类型参数名名可以用来指定函数模板的返回值; 模板参数名在同一模板参数表中只能被使用一次, 但是模板参数名可以在多个函数模板声明或定义之间重复使用; 一个模板的定义和多个声明所使用的模板参数不需要相同; 模板参数在函数参数表中出现的次数没有限制; 每个模板类型参数前面都必须有关键字class或typename;
函数模板参数表中typename和class意义相同; 都可以用来声明模板参数表中的不同模板类型参数;<<Design and Evolution of C++>>
Note 为了让编译器能够分析模板定义, 用户必须指示编译器, 哪些表达式是类型表达式: typename;
template <class Parm, class U>
Parm minus(Parm* array, U value)
{
typename Parm::name* p;//指针声明
Parm::name* p; //编译器不知道这是指针声明还是乘法
}
Note 函数模板声明为inline或extern时, 指示符放在模板参数表后面, 不能放在关键字template前面;
10.2 函数模板实例化
函数模板指定实际类型或值构造出独立函数的过程: 模板实例化template instantiation;
这个过程是隐式发生的, 可看作是函数模板被调用或取函数模板的地址的副作用;
int (*pf)(int (&)[10]) = &min;// min(int(&)[10]);
编译器会检查函数调用中提供的函数实参的类型. 用函数实参的类型来决定模板实参的类型和值的过程被称为模板实参推演template argument deduction;
Note 在取函数模板实例的地址时, 必须通过上下文为模板实参决定一个唯一的类型或值;
typedef int (&rai)[10];
typedef double (&rad)[20];
void func( int (*)(rai) ){}
void func( double (*)(rad) ){}
//---
func(&min);//error, overload-function
>编译错误: func()被重载了, 编译器无法决定Type的唯一类型, 也无法决定size的唯一值; 调用func()无法实例化函数;
10.3 模板实参推演
函数模板调用时, 对函数实参类型的检查决定模板实参的类型和值, 这个过程称为模板实参推演template argument deduction;
>函数实参必须是一个数组类型的左值; 这里的pval是函数参数, 所以转换为指针变量, 与数组左值类型不匹配; (传参数时数组->指针匹配, 指针->数组不行)
template <class Type, int size>
Type min( Type (&r_array)[size] ) { /* ... */ }
void f( int pval[9] ) {
// 错误: Type (&)[] != int*
//int jval = min( pval );
}
模板实参推演时, 函数的实参类型不一定要严格匹配函数参数类型;
允许的类型转换: 左值转换, 限定转换, 到一个基类的转换;
左值转换包括: 左值到右值的转换, 数组到指针的转换, 函数到指针的转换;
函数模板实参推演的通用算法:
1)依次检查每个函数实参, 确定在每个函数实参的类型中出现的模板参数;
2)找到模板参数, 通过检查函数实参的类型, 推演相应的模板实参;
3)函数参数类型和函数实参类型可以转换: -左值转换 -限定修饰符转换 - 派生类到基类的转换;
4)如果在多个函数参数中找到同一个模板参数, 从每个相应函数实参推演出的模板参数必须相同(模板参数会被绑定在第一个类型上)
10.4 显示模板实参
显示指定explicitly specify 模板实参;
e.g. func<usigned int>(1024); //1024被有序标准转换成unsigned int类型;
当函数模板实参被显示指定时, 函数实参转换成相应函数参数的类型可以应用任何隐式类型转换;
显式特化explicit specification, 只能省略尾部的实参;
char ch; unsigned int ui;
template <class T1, class T2, class T3>
T1 sum( T2, T3 );
typedef unsigned int ui_type;
ui_type loc1 = sum( ch, ui );//error, T1不能被推演
ui_type loc2 = sum< ui_type, char, ui_type >( ch, ui );//ok
ui_type loc3 = sum< ui_type, char >( ch, ui );//ok
ui_type (*pf)( char, ui_type ) = &sum< ui_type >;//ok
ui_type loc4 = sum< ui_type, , ui_type >( ch, ui );//error, 只能省略尾部的实参
>对于重载函数的函数指针类型实参, 为避免二义性, 使用显式指定;
template <class T1, class T2, class T3>
T1 sum( T2 op1, T3 op2 ) { /* ... */ }
void manipulate( int (*pf)( int,char ) );
void manipulate( double (*pf)( float,float ) );
int main( )
{
manipulate( &sum );// 错误: 二义
manipulate( &sum< double, float, float > );// 调用: void manipulate( double (*pf)( float, float ) );
}
建议在可能的情况下省略显式模板实参, 让函数功能更泛化;
Note 返回值无法推演, 推演只作用在参数上, 如果返回值和参数相同, 则可以被间接推演;
10.5 模板编译模式
C++模板编译模式 template compilation model 指定了对于定义和使用模板的程序的组织方式的要求;
编译模式: 包含模式 Inclusion Model 和 分离模式 Separation Model;
10.5.1 包含编译模式
模板定义在头文件中, 使用模板实例前包含模板定义;
缺点: 模式模板体body描述了实现细节, 这些是我们希望对用户隐藏的;
函数模板定义很大的情况下, 头文件中的细节层次变得不可接受;
多个头文件之间编译相同的函数模板定义增加编译时间;
10.5.2 分离编译模式
函数模板声明在头文件中;
// model2.h // 分离模式: 只提供模板声明
template <typename Type> Type min( Type t1, Type t2 );
// model2.C // the template definition
export template <typename Type>
Type min( Type t1, Type t2 ) { /* ...*/ }
// user.C
#include "model2.h"
int i, j;
double d = min( i, j ); //用法, 需要一个实例
>关键字export告诉编译器在生成被其他文件使用的函数模板实例时需要这个模板定义(可导出的函数模板), 编译器需要保证模板定义是可见的;
(有些编译器可能不要求export; 标准C++要求在template之前标记export)
关键字export在头文件的模板声明中不是必需的;(在cpp实现中需要)
一个模板函数只能被定义为export一次; 编译器每次只处理一个文件, 无法检测到模板函数在多个文本文件中定义为export的情况; 可能发生的情况:
1)链接错误, 函数模板在多个文件中被定义;
2)编译器多次为同一个模板实参集合实例化函数模板, 函数模板实例重复定义, 链接错误;
3)编译器可能只用其中一个export函数模板定义实例化函数模板, 忽略其他定义;
Note 不是所有的编译器都支持分离模式; (VC2010, g++不支持)
10.5.3 显式实例化声明
显式实例化声明帮助控制并减少编译器对模板多次实例化的情况, 减少编译时间;
template <typename Type>
Type sum( Type op1, int op2 ) { /* ... */ }
template int* sum<int*>(int*, int);
显式实例化声明在程序中只能出现一次, 函数模板的定义必须可见;
显式实例化声明是与一个编译选项联合使用的, 该选项压制程序中模板的隐式实例化; e.g. /ft-; (这个模式下显式实例化声明是必需的)
10.6 模板显式特化
模板显式特化定义explicit specialization definition;
template <class T>
T max( T t1, T t2 ) {
return (t1 > t2 ? t1 : t2);
}
//特化 specialize
typedef const char *PCC;
template<> PCC max< PCC >( PCC s1, PCC s2 ) {
return ( strcmp( s1, s2 ) > 0 ? s1 : s2 );
}
>由于有了显式特化, 程序在调用max()时模板不对const char*类型进行实例化, 而是直接调用特化的定义;
如果模板实参可以从函数参数中推演出来, 模板实参的显式特化可以省略
template<> PCC max( PCC s1, PCC s2 )
>省略尖括号"<>"部分;
使用函数模板特化时, 必须能让所有使用特化的文件看到其声明, 否则编译器找不到特化, 会实例化基模板;
如果同一程序在一个文件中实例化函数模板实例(声明不可见), 在另一个文件中又调用其显式特化, 程序非法;
Note: 显式特化的声明应该被放在头文件中, 并且在所有使用函数模板的程序中包含此头文件;
10.7 重载函数模板
重载的函数模板可能会导致二义性;
template <typename T>
int min5( T, T ) { /* ... */ }
template <typename T, typename U>
int min5( T, U );
>对于类似min(1024, 1); 这样的调用, T和U可以是同为int型, 这样两个模板都可以被实例化; 需要显式指定模板实参防止二义性错误;
>min5(T,U);处理的调用集是由min5(T,T)处理的超集, 所以只提供min5(T,U)的声明就可以, min5(T,T)应该删除;
最特化most specialized 相同模板名字和参数个数; 对于不同类型的相应函数参数, 一个模板是另一个的超集;
template <typename Type> Type sum( Type*, int );//1)
template <typename Type> Type sum( Type, int );//2)
int ia[1024];
// Type == int ; sum<int>( int*, int ); or
// Type == int*; sum<int*>( int*, int ); ??
int ival1 = sum<int>( ia, 1024 );
>1)更特化; 2)是1)的超集;
10.8 考虑模板函数实例的重载解析
函数模板可以被重载, 函数模板可以与普通函数同名;
// 普通函数
int min( int a1, int a2 ) {
min<int>( a1, a2 );
}
>通过普通函数, 无论何时使用整型实参, 程序都调用min(int,int)的特化版本;
普通函数和函数模板的函数重载解析步骤:
1)生成候选函数集;
考虑与函数调用同名的函数模板, 如果模板实参推演成功则实例化一个函数模板, 则该模板作为一个候选函数;
2)生成可行函数集;
保留候选函数集中可以调用的函数;
3)对类型转换划分等级;
a)如果只选择了一个函数, 则调用该函数; b)如果调用是二义的, 则从可行函数集中去掉函数模板实例;
4)只考虑可行函数集中的普通函数, 完成重载解析过程;
a)如果只选择了一个函数, 则调用该函数; b)否则是二义的;
10.9 模板定义中的名字解析
类型依赖于模板参数depend on a template parameter;
模板定义中的名字解析分两个步骤进行:
首先, 不依赖于模板参数的名字, 在模板定义时被解析; (普通函数 e.g. print("string");)
其次, 依赖于模板参数的名字, 在模板被实例化时被解析; (依赖于模板的函数; e.g. print(Type t);)
Note 函数模板的设计者必须确保为模板定义中用到的, 所有不依赖于模板参数的名字提供声明; 否则编译错误;
在源代码中模板被实例化的位置称为模板的实例化点point of instantiation;
如果模板实例要多次使用, 编译器自由选择这些实例化点之一来真正实例化该函数模板; 因此在模板的任何一个实例被使用之前, 应该在头文件中给出所有必须的声明;
10.10 名字空间和函数模板
函数模板定义可以放在名字空间中;
namespace cplusplus_primer {
// 模板定义被隐藏在名字空间中
template<class Type>
Type min( Type* array, int size ) { /* ... */ }
}
int ai[4] = { 12, 8, 73, 45 };
int size = sizeof(ai) / sizeof(ai[0]);
using cplusplus_primer::min; // using 声明
min( &ai[0], size );
作为包含模板定义的库的用户, 如果想为库中的模板提供特化, 必须保证它们的定义被合适地放置在含有原始模板定义的名字空间内;
1)把模板特化放在基模板所在的名字空间内;
2)用外围名字空间名修饰名字空间成员名;
template<> SmallInt cplusplus_primer::min<SmallInt>( SmallInt* array, int size ){ /* ... */ }
10.11 函数模板示例
快速排序sort();
#include "Array.h"
template <class elemType>
void sort( Array<elemType> &array, int low, int high ) {
if ( low < high ) {
int lo = low;
int hi = high + 1;
elemType elem = array[lo];//基数
for (;;) {
while ( min( array[++lo], elem ) != elem && lo < high ) ;
while ( min( array[--hi], elem ) == elem && hi > low ) ;
if (lo < hi)
swap( array, lo, hi );//较大数放右边, 较小数放左边
else break;
}
swap( array, low, hi );//基数放中间
//分治递归
sort( array, low, hi-1 );
sort( array, hi+1, high );
}
}
---End---