定义模板——函数模板和类模板
面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP能处理类型在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了。
前面介绍的容器、迭代器和算法都是泛型编程的例子。当我们编写一个泛型程序时,是独立与任何特定类型来编写代码的。当使用一个泛型程序时,我们提供类型或值,程序实例可在其上运行。
模板是泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。当使用一个vector这样的泛型类型,或者find这样的泛型函数时,我们提供足够的信息,将蓝图转换为特定的类型函数。这种转换发生在编译时。
定义模板
假定我们希望编写一个函数来比较两个值,并指出第一个值是小于、等于还是大于第二个值。在实际中,我们可能想要定义多个函数,每个函数比较一个给定类型的值。我们的初次尝试可能定义多个重载函数:
int compare(const string &v1,const string &v2) { if(v1<v2) return -1; if(v1>v2) return 1; return 0; } int compare(const double &v1,const string &v2) { if(v1<v2) return -1; if(v1>v2) return 1; return 0; }
这两个函数几乎相同,唯一的差别是参数的类型,函数体则完全一样。
如果对每种希望比较的类型都不得不重复定义完全一样的函数体,是非常烦琐且容易出错的。更麻烦的是,在编写程序的时候,我们就要确定可能要compare的所有类型。
函数模板
我们可以定义一个通用的函数模板,而不是为每个类型都定义一个新函数。一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。compare的模板版本可能像下面这样:
template<typename T> int compare(const T&v1,const T &v2) { if(v1<v2) return -1; if(v1>v2) return 1; return 0; }
模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的类表,用小于号(<)和大于号(>)包围起来。
在模板定义中,模板参数列表不能为空。
模板参数列表的作用很像函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时,调用者提供实参来初始化形参。
类似的,模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们(隐式地或显式地)指定模板实参(template argument),将其绑定到模板参数上。
我们的compare函数声明了一个名为T的类型参数。在compare中,我们用名字T表示一个类型。而T 表示的实际类型则在编译时根据compare的使用情况来确定。
实例化函数模板
当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。即,当我们调用compare时,编译器使用实参的类型来确定绑定到模板参数T的类型。例如,在下面的调用中:
cout<<compare(1,0)<<endl; //T为int
实参类型是int。编译器会推断出模板实参为int,并将它绑定到模板参数T。
编译器用推断出的模板参数来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。
例如,给定下面的调用:
//实例化出int compare(const int&,const int&)
cout<<compare(1,0)<<endl; //T为int
//实例化出 int compare(const vector<int>&,const vector<int>&)
vector<int> vec1(1,2,3),vec2(4,5,6);
cout<<compare(vec1,vec2)<<endl; //T为vector<int>
编译器会实例化出两个不同版本的compare。对于第一个调用,编译器会编写并编译一个compare版本,其中T被替换为int:
int compare(const int &v1,const int &v2) { if(v1<v2) return -1; if(v1>v2) return 1; return 0; }
对于第二个调用,编译器会生成另一个compare版本,其中T被替换为vector<int>,这些编译器生成的版本通常被称为模板的实例。
模板类型参数
我们的compare函数有一个模板类型参数,一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
类型参数必须使用关键字class或typename:
//错误:U之前必须加上class或typename
template <typename T,U> T calc(const T&,const U&);
在模板参数列表中,这两个关键字的含义相同,可以相互转换使用。一个模板参数列表中可以同时使用这两个关键字:
//正确:在模板参数列表中,typename和class没有什么不同
template<typename T,class U> calc(const T&,const U&);
看起来用关键字typename来指定模板类型参数比用class更为直观。毕竟,我们可以用内置(非类)类型作为模板类型实参。而且,typename更清楚地指出随后的名字是一个类型名。
非类型模板参数
除了定义类型参数,还可以在模板定义中定义非类型参数。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class 或 typename来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。