强类型语言要求我们为所有希望比较的类型都实现一个实例:
int min( int a, int b )
{
return a < b ? a : b;
}
double min( double a, double b )
{
return a < b ? a : b;
}
有一种方法可替代这种“为每个min()实例都显式定义一个函数”的方法。这种方法很有吸引力,但是也很危险,那就是用预处理器的宏扩展设施。例如:
#define min(a,b) ((a) < (b) ? (a) : (b))
虽然该定义对于简单的min()调用都能正常工作。如:
min( 10, 20 );
min( 10.0, 20.0 );
但是,在复杂调用下,它的行为是不可预期的。这是因为它的机制并不像函数调用那样工作,而只是简单地提供参数的替换。结果是,它的两个参数值都被计算两次。一次是在a 和b 的测试中,另一次是在宏的返回值被计算期间。例如:
#include <iostream>
#define min(a,b) ((a) < (b) ? (a) : (b))
const int size = 10;
int ia[size];
int main()
{
int elem_cnt = 0;
int *p = &ia[0];// 计数数组元素的个数
while ( min(p++,&ia[size]) != &ia[size] )
++elem_cnt;cout << "elem_cnt : " << elem_cnt << "\texpecting: " << size << endl;
return 0;
}
在指针实参p 上的后置递增操作随每次扩展而被应用了两次,执行结果是下面不正确的计算结果:
elem_cnt : 5 expecting: 10
函数模板定义
模板提供了一种机制,通过它我们可以保留函数定义和函数调用的语义(在一个程序位置上封装了一段代码,确保在函数调用之前实参只被计算一次)而无需像宏方案那样绕过C++的强类型检查。
关键字template 总是放在模板的定义与声明的最前面。关键字后面是用逗号分隔的模板参数表,它用尖括号<> 括起来。该列表是模板参数表,不能为空。模板参数可以是一个模板类型参数( template type
parameter),它代表了一种类型,也可以是一个模板非类型参数( template nontype parameter),它代表了一个常量表达式。
模板类型参数由关键字class 或typename 后加一个标识符构成。在函数的模板参数表中,这两个关键字的意义相同。它们表示后面的参数名代表一个潜在的内置或用户定义的类型。模板参数名由程序员选择,在本例中,我们用Type 来命名min()的模板参数。但实际上可以是任何名字。譬如:
template <class Glorp>
Glorp min( Glorp a, Glorp b )
{
return a < b ? a : b;
}
当模板被实例化时,实际的内置或用户定义类型将替换模板的类型参数。
非类型参数由一个普通的参数声明构成。模板非类型参数表示该参数名代表了一个潜在的值,而该值代表了模板定义中的一个常量。例如,size 是一个模板非类型参数,它代表arr 指向的数组的长度。
template <class Type, int size>
Type min( Type (&arr) [size] );
当函数模板min()被实例化时,size 的值会被一个编译时刻已知的常量值代替。
函定义或声明跟在模板参数表后,除了模板参数是类型指示符或常量值外,函数模板的定义看起来与非模板函数的定义相同。
在程序的运行过程中,Type 会被各种内置类型和用户定义的类型所代替。而size 会被各种常量值所取代,这些常量值是由实际使用的min()决定的(记住,一个函数的两种用法是调用它和取它的地址 )。类型和值的替换过程被称为模板实例化(template instantiation)。
当一个名字被声明为模板参数之后,它就可以被使用了,一直到模板声明或定义结束为止。模板类型参数被用作一个类型指示符,可以出现在模板定义的余下部分,它的使用方式与内置或用户定义的类型完全一样,比如用来声明变量和强制类型转换。模扳非类型参数被用作一个常量值,可以出现在模板定义的余下部分,它可以用在要求常量的地方,或许是在数组声明中指定数组的大小或作为枚举常量的初始值。
// size 指定数组参数的大小并初始化一个 const int 值
template <class Type, int size>
Type min( const Type (&r_array)[size] )
{
const int loc_size = size;
Type loc_array[loc_size];
// ...
}
函数模板的异常处理
函数模板中的模板形参可实例化为各种类型,但当实例化模板形参的各模板实参之间不完全一致时,就可能发生错误,如:
template<typename T>
void min(T &x, T &y)
{ return (x<y)?x:y; }
void func(int i, char j)
{
min(i, i);
min(j, j);
min(i, j);
min(j, i);
}
例子中的后两个调用是错误的,出现错误的原因是,在调用时,编译器按最先遇到的实参的类型隐含地生成一个模板函数,并用它对所有模板函数进行一致性检查,例如对语句
min(i, j);
先遇到的实参i是整型的,编译器就将模板形参解释为整型,此后出现的模板实参j不能解释为整型而产生错误,此时没有隐含的类型转换功能。解决此种异常的方法有两种:
⑴采用强制类型转换,如将语句min(i, j);改写为min(i,int( j));
⑵用非模板函数重载函数模板
方法有两种:
① 借用函数模板的函数体
此时只声明非模板函数的原型,它的函数体借用函数模板的函数体。如改写上面的例子如下:
template<typename T>
void min(T &x, T &y)
{ return (x<y)?x:y; }
int min(int,int);
void func(int i, char j)
{
min(i, i);
min(j, j);
min(i, j);
min(j, i);
}
执行该程序就不会出错了,因为重载函数支持数据间的隐式类型转换。
② 重新定义函数体
就像一般的重载函数一样,重新定义一个完整的非模板函数,它所带的参数可以随意。C++中,函数模板与同名的非模板函数重载时,应遵循下列调用原则:
-
寻找一个参数完全匹配的函数,若找到就调用它。若参数完全匹配的函数多于一个,则这个调用是一个错误的调用。
-
寻找一个函数模板,若找到就将其实例化生成一个匹配的模板函数并调用它。
-
若上面两条都失败,则使用函数重载的方法,通过类型转换产生参数匹配,若找到就调用它。
-
若上面三条都失败,还没有找都匹配的函数,则这个调用是一个错误的调用。
几个需要注意的地方:
-
如果在全局域中声明了与模板参数同名的对象、函数、或类型,则该全局名将被隐藏。在下面的例子中tmp的类型不是double ,而是模板参数Type:
typedef double Type;
template <class Type>
Type min( Type a, Type b )
{
// tmp 类型为模板参数Type,不是全局 typedef
Type tmp = a < b ? a : b;
return tmp;
}
-
在函数模板定义中声明的对象或类型不能与模板参数同名。
template <class Type>
Type min( Type a, Type b )
{
// 错误: 重新声明模板参数 Type
typedef double Type;
Type tmp = a < b ? a : b;
return tmp;
}
-
模板类型参数名可以被用来指定函数模板的返回值。
// ok: T1 表示 min() 的返回类型,T2 和 T3 表示参数类型
template <class T1, class T2, class T3>
T1 min( T2, T3 );
-
模板参数名在同一模板参数表中只能被使用一次,但是模板参数名可以在多个函数模板声明或定义之间被重复使用。
// 错误: 模板参数名 Type 的非法重复使用
template <class Type, class Type>
Type min( Type, Type );
// ok: 名字 Type 在不同模板之间重复使用
template <class Type>
Type min( Type, Type );
template <class Type>
Type max( Type, Type );
-
如果一个函数模板有一个以上的模板类型参数,则每个模板类型参数前面都必须有关键字class或typename。
// ok: 关键字 typename 和 class 可以混用
template <typename T, class U>
T minus( T*, U );
// 错误: 必须是 <typename T, class U> 或 <typename T, typename U>
template <typename T, U>
T sum( T*, U );
-
为了分析模板定义,编译器必须能够区分出是类型以及不是类型的表达式。对于编译器来说,它并不总是能够区分出模板定义中的哪些表达式是类型。例如,如果编译器在模板定义中遇到表达式Parm::name ,且Parm 这个模板类型参数代表了一个类。那么name 引用的是Parm 的一个类型成员吗.
template <class Parm, class U>
Parm minus( Parm* array, U value )
{
Parm::name * p; // 这是一个指针声明还是乘法?
}
编译器不知道name 是否为一个类型,因为它只有在模板被实例化之后才能找到Parm 表示的类的定义。为了让编译器能够分析模板定义,用户必须指示编译器哪些表达式是类型表达式,告诉编译器一个表达式是类型表达式的机制是在表达式前加上关键字typename。例如,如果我们想让函数模板minus()的表达式Parm::name 是个类型名,因而使整个表达式是一个指针声明。我们应如下修改:
template <class Parm, class U>
Parm minus( Parm* array, U value )
{
typename Parm::name * p; // ok: 指针声明
}
关键字typename 也可以被用在模板参数表中,以指示一个模板参数是一个类型。
-
如同非模板函数一样,函数模板也可以被声明为inline 或extern。应该把指示符放在模板参数表后面,而不是在关键字template 前面。
// ok: 关键字跟在模板参数表之后
template <typename Type>
inline Type min( Type, Type );