模板是泛型编程的基础,所谓泛型编程就是用独立于任何特定类型的方式编写代码。在C++里,常说的多态一般分为两种:
一种是运行时的多态,也就是虚函数体现的多态。
另一种是编译时的多态,也就是泛型编程的多态,体现在参数的多态。
所谓参数多态性,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象。
函数模板
函数模板的声明是在关键字 template 后跟随一个或多个模板在尖括弧内的参数和原型。每一个类型参数(T)之前都有关键字class或者关键字typename,这些类型参数代表的是类型,可以是内部类型或自定义类型,这样,类型参数就可以用来指定函数模板本身的参数类型和返回值类型,以及声明函数中的局部变量。与普通函数相对,它通常是在一个转换单元里声明,而在另一个单元中定义,你可以在某个头文件中定义模板。
例子:
template <typename T>
T mymax(T a, T b) {
returna > b ? a : b;
}//可把typename换成class
•函数模版的调用
int main() {
cout<<mymax(3, 4)<<endl;
cout<<mymax(3.1, 4.0)<<endl;
stringa = “roba”, b= “acm”;
cout<<mymax(a,b)<<endl;
}
函数模板的几点注意:
1、如果在全局域中声明了与模板参数同名的对象函数或类型,则该全局名将被隐藏,例子如下:
typedef double Type;
template <class Type>
Type min( Type a, Type b )
{
// tmp 类型为模板参数 Type
// 不是全局 typedef
Type tmp= a < b ? a : b;
returntmp;
}
2、在函数模板定义中声明的对象或类型不能与模板参数同名
template <class Type>
Type min( Type a, Type b )
{
// 错误: 重新声明模板参数 Type
typedef double Type;
Type tmp= a < b ? a : b;
returntmp;
}
3、模板类型参数名可以被用来指定函数模板的返回位
// ok: T1 表示 min() 的返回类型
// T2 和 T3 表示参数类型
template <class T1, class T2, class T3>
T1 min( T2, T3 );
4、模板参数名在同一模板参数表中只能被使用一次,但是模板参数名可以在多个模板声明或定义之间被重复使用
// 错误: 模板参数名 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 );
5、如果一个函数模板有一个以上的模板类型参数,则每个模板类型参数前面都必须有关键字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 );
6、为了分析模板定义,编译器必须能够区分出是类型以及不是类型的表达式,对于编译器来说它并不总是能够区分出模板定义中的哪些表达式是类型,例如如果编译器在模板定义中遇到表达式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也可以被用在模板参数表中以指示一个模板参数是一个类型
7、如同非模板函数一样函数模板也可以被声明为inline或extern,应该把指示符放在模板参数表后面
// ok: 关键字跟在模板参数表之后
template <typename Type>
inline
Type min( Type, Type );
8、与宏替换不同,函数模板是在编译期“实例化”
9、隐式指定参数类型,如最上面例子
cout << mymax(3, 4.1) << endl; //错误
10、显式指定参数类型:
mymax<int>(3, 4);
mymax<double>(3, 4.1); //正确,3被转换成double型
类模板
类模板是对一批仅仅成员数据类型不同的类的抽象,程序员只要为这一批类所组成的整个类家族创建一个类模板,给出一套程序代码,就可以用来生成多种具体的类,(这类可以看作是类模板的实例),从而大大提高编程的效率。
使用类模板使用户可以为类声明一种模式,使得类中的某些数据成员、某些成员函数的参数、某些成员函数的返回值能取任意类型(包括系统预定义的和用户预定义的)
模板的类型参数由关键字class或关键字typename及其后的标识符构成。在模板参数表中关键字class和typename的意义相同。(在标准C++之前关键字typename没有被支持,把这个关键字加入到C++中的原因是因为有时必须要靠它来指导编译器解释模板定义。)
例子
类模板是一个类家族的抽象,它只是对类的描述,编译程序不为类模板(包括成员函数定义)创建程序代码,但是通过对类模板的实例化可以生成一个具体的类以及该具体类的对象。
与函数模板不同的是:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定,其实例化的一般形式是:
模板元编程(Template MetaProgramming)
是一种元编程技术,编译器使用模板产生暂时性的源码,然后再和剩下的源码混合并编译。这些模板的输出包括编译时期常数、数据结构以及完整的函数。如此利用模板可以被想成编译期的运行。
使用模板作为元编程的技术需要两阶段的操作:
首先,模板必须定义,第二,定义的模板必须被实体化才行。
模板元编程没有可变的变量--也就是说,变量一旦初始化后就不能改动。因此他可以被视为函数式编程(functional programming)的一种形式。
使用模板元编程的用途:
实现泛型编程或展现自动编译期优化
编译期类型生成
template <int N>
struct Factorial
{
enum { value = N * Factorial<N - 1>::value };
};
template <>
struct Factorial<0>
{
enum { value = 1 };
};
// Factorial<4>::value == 24
// Factorial<0>::value == 1
void foo()
{
int x = Factorial<4>::value; // == 24
int y = Factorial<0>::value; // == 1
}
代码如上在编译时期计算4和0的阶乘值,使用该结果放佛他们是预定的常数一般。
编译期代码优化
以上的阶乘也是编译期代码优化的一例,该程序中使用到的阶乘在编译期时便被预先计算并作为数值常数植入运行码中,节省了运行期的经常开销以及存储器足迹。
编译期循环展开是一个好的例子,模板元编程可以被用来产生n维的矢量类型,例子如下:
template <int dimension>
Vector<dimension>& Vector<dimension>::operator+=(const Vector<dimension>& rhs)
{
for (int i = 0; i < dimension; ++i)
value[i] += rhs.value[i];
return *this;
}
当编译器实体化以上的模板函数,可能会生成如下的代码:
template <>
Vector<2>& Vector<2>::operator+=(const Vector<2>& rhs)
{
value[0] += rhs.value[0];
value[1] += rhs.value[1];
return *this;
}
因为模板参数dimension在编译期是常数,所以编译器应能展开for循环。