模板是泛型编程的基础。
定义模板
我们希望编写一个程序来比较两个值的大小。
这样的两个值可能是int、float、double、string,这样就需要需要不断地编写函数来实现对类型的重载。
其实这些函数只有形参的类型不一样,那么模板就能很好地解决这一问题。
函数模板
template <typename T>
int compare(const T &v1, const T &v2){
...
}
模板定义以关键字template开始,后跟一个模板参数列表。这是一个逗号分隔的一个或多个模板参数,用<>包围起来。
模板定义中,模板参数列表不能为空。
模板参数
当我们调用一个函数模板时,编译器用函数实参来为我们推断模板实参。比如,compare(0,1) ,会默认将模板实参推断为int,并绑定到模板参数T。
编译器用推断出的模板参数来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新实例。
这些编译器生成的版本通常被称为模板的实例。
模板类型参数
模板类型参数列表中的类型参数可以看做类型说明符。
类型参数前必须使用关键字class或者typename。在模板参数列表中,这两个关键字含义相同,可以互换使用。甚至一个模板参数列表中可以同时使用这两个关键字。
template <typename T, class U>
calc();
非类型模板参数
除了可以定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。
我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。
template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]){
return strcmp(p1,p2);
}
compare("hi", "mom");
模板非类型参数是一个常量。
编写类型无关的代码
compare函数说明了编写泛型代码的两个重要原则:
- 模板中的函数参数是const的引用;
- 函数体中的条件判断仅使用<比较运算。
第一个原则用于处理不能拷贝的类型;第二个原则考虑到一些只支持<符号的情况。
这样大大提高了函数的鲁棒性,减少对实参类型的要求,这符合模板程序的思想。
模板编译
当编译器遇到一个模板定义时,并不生成代码,只有实例化时,编译器才会生成代码。
当我们使用一个类类型对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件,而普通函数和类的成员函数定义放在源文件。
模板则不同,模板的头文件通常既包括声明也包括定义。
类模板
与函数模板不同的是,编译器不能为类模板推断模板参数类型。我们必须在模板名后的尖括号中,提供额外信息。
定义类模板
template <typename T> class Blob{
...
};
实例化类模板
主动提供的这些额外信息时显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实例来实例化出特定的类。
Bolb<int> ia;
类模板的成员函数
我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。
类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而定义在模板之外的成员函数就必须以关键字template开始,后接模板参数列表。
template <typename T>
void Blob<T>::check(){...;}
模板参数
一个模板参数的名字没有什么内在含义,通常命名为T。
模板参数与作用域
一个模板参数名的可用范围是在其声明后至模板声明或定义结束之前。
与其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是模板内不能重用模板参数名。
typedef double A;
template<typename A,B> void f(A a, B b){
A temp = a;
double B;
}
默认模板实参
template <typename T, typename F = less<T>>
...
成员模板
一个类可以包含本身是模板的成员函数。这种成员函数被称为成员模板。成员模板不能是虚函数。
|