C++模板
C++程序是一些类型和函数,编程就是设计类型和函数,然后将它们按C++的程序结构组织起来。由于事物的相似性,设计的类型和函数有时也表现出相同性。将这些相似的类型和函数归纳起来构成一个类簇和函数簇,用一种统一的方式来编程就是模板编程。由模板可以得到一系列的相似类型或相似函数,这些相似类型和相似函数涉及到的数据类型可能不同,但处理数据却具有相同的表现形态。这些相似类型和相似函数就是将涉及到的数据之类型为参数来生成的模板,其相似类型称为模板类,相似函数称为模板函数。
一·函数模板(Function Templates)
考察两个交换函数swap,一个swap交换两个int类型的值,一个swap交换两个double类型的值,分别如下:
void swap(int& a,int& b)
{
int temp = a;
a = b;
b = temp;
}
void swap(double& a,double& b)
{
double temp = a;
a = b;
b = temp;
}
事实上,交换任何两个Type类型的对象,都有下列函数定义形式:
void swap(Type& a,Type& b)
{
Type temp = a;
a = b;
b = temp;
}
不同的Type类型,可以写出不同的swap函数,这些交换函数都是重载的,或者说,都是函数同名的。这上系列swap重载函数,随着所处理的数据类型的变更,其函数实体会像一个个零星的小碎片一样,分散在需要该操作的程序上下文中,增加了工作量。
1.函数模板的定义(Define Function Template)
函数模板的定义形式为
template <typename 类型参数表>
返回类型 函数模板名(数据参数表)
{
函数模板定义体
}
template后面用尖括号括起来的"类型参数表",描述函数模板"函数模板名"的模板形式参数(简称模板形参)。模板形参是类型形式的,可以是基本数据类型,也可以是类类型。每个模板形参都必须加上前缀class或typename(typename更确切).在template描述的模板形参之后是函数模板的定义体,它包括模板返回在类型,函数模板名,函数模板之数据形参,以及函数模板定义体。上面的swap函数族可以写成函数模板:
template<typename T>
void swap(T& a,T& b)
{
T temp = a;
a = b;
b = temp;
}
其中,函数模板名为swap,模板形参为T,函数模板的数据形参为a和b,函数模板的返回类型为void,函数模板的定义体为一对花括号中间的内容。
函数模板名后面圆括号中的数据形参一般要用到template后面的模板形参名T,也就是具有模板形参T的对象或变量实体。这里的swap函数模板中,数据形参表就是由具有T的引用类型的对象a和b构成的。
函数模板不是函数,它是以具体的类型为实参来生成函数体的模板。函数模板定义被编译时,不会产生任何执行代码。函数模板定义只是对未来生成的函数体的描述,表示它每次能单独处理在模板形参表中说明的数据类型。
2.函数模板的用法(Using Function Templates)
使用函数模板,就是以函数模板名为函数名的函数调用。其形式为:
函数模板名(数据实参表);
第一次使用函数模板时,会触发编译器产生一个对应函数模板的函数体定义。当编译器发现有一个函数模板名为函数名的调用时,将根据数据实参表中的对象或变量的类型,确认是否匹配函数模板中对应的数据形参表,然后生成一个函数。该函数的定义体与函数模板的定义体相同,而数据形参表的类型则以数据实参表的类型为依据。该函数称为模板函数。例:
//////01.cpp
1.#include<iostream>
2.using namespace std;
3.template<typename T>
4.void swap(T& a,T& b)
5.{
6. T temp = a;a = b;b = temp;
7.}
8.int main()
9.{
10. double dx = 3.5,dy = 3.6;
11. int ix = 6,iy = 7,ia = 303,ib = 505;
12. string s1 = "good",s2="better";
13. cout<<"double dx="<<dx<<", dy="<<dy<<"\n";
14. cout<<"int ix ="<< ix<<", iy ="<< iy<<"\n";
15. cout<<"string s1 =\"<<s1<<"\", s2 =\""<<s2<<"\"\n";
16. swap(dx,dy);
17. swap(ix,iy);
18. swap(s1,s2);
19. swap(ia,ib);
20. cout<<"\after swap:\n";
21. cout<<"double dx="<<dx<<", dy="<<dy<<"\n";
22. cout<<"int ix="<<ix<<x, iy="<<iy<<"\n";
23. cout<<"string s1=\""<<s1<<"\",s2=\""<<s2<<"\"\n";
24.}
编译器在看到第16行的swap(dx,dy)时,因为是首次看到double型实参的模板名swap的函数调用,所以生成函数名为swap<double>的模板函数,即生成如下形式的函数定义:
void swap<double>(double& a,double& b)
{
double temp= a;a = b;b = temp;
}
由于dx和dy是double型的,因此该double类型再推演,得到其模板实参为double型。因此模板函数得以命名为swap<double>。同时,数据实参dx和dy作为初值赋给了数据形参a和b.根据数据实参的类型----匹配数据形参的类型---确认模板实参----推得模板形参的过程称为数据实参的演绎。
以函数模板名为函数名的函数调用,以数据实参dx和dy推演函数模板实参,进而生成模板定义的过程称为函数模板的实体化或实例化。同样,在第17行中的swap(ix,iy)调用时,生成swap<int>模板函数定义。但是,当第19行上又一次看到swap(ia,ib)的函数调用时,由于系统中已经存在int型的swap模板函数,所以就不再生成swap<int>的模板函数定义了。模板函数定义也是函数定义的一种,必须符合C++函数的一次定义规则。
显然,一个函数模板可以生成许多不同的模板函数,如函数模板swap生成了模板函数swap<double>和swap<int>。因此,一个函数模板所能生成的都是不同名称的模板函数,模板实参不同,则生成的模板函数也不同,一个函数模板所反映的是不同函数的函数族,它们因类型实参不同而不同。
二.函数模板参数(Function Templae Parameters)
1.苛刻的类型匹配
模板函数调用是寻求函数模板的类型参数匹配,类型实参与类型形参的匹配规则与函数的数据实参类型与数据形参类型匹配规则不同。类型实参与类型形参匹配规则更苛刻。例:
///////02.cpp
1.template<typename T>
2.void swap(T& a,T& b)
3.{
4. T temp = a;a = b;b = temp;
5.}
6.int add(double a,double b)
7.{
8. return a+b;
9.}
10.int main()
11.{
12. int ia = 3;
13. double db = 5.0;
14. char s1[] = "good",s2[]="better";
15. int x = add(ia,db); //ok
16. swap(ia,db); //NG
17. swap(s1,s2); //NG
18.}
第15行add(ia,db)函数的调用是变通函数调用,虽然ia的类型与double型不同,但通过数据的int型隐式转换到double型,实现了合法调用 。
而第16行的swap(ia,db)函数调用,由于ia和db的类型分别为int型和double型,不能统一到同一个类型上,且模板类型参数没有隐式转换之说,拒绝按假想转换到doubl型或是int型,必须精确匹配,所以引起编译错误。
第17行的swap(s1,s2)函数调用,更是离谱。对于引用性形式参数T&a来说,字符数组s1和s2甚至不被看作是字符指针,s1和s2的类型这字符数组char[5]和char[7],由于5不等于7,所以不是同种类型,按照模板类型参数精确匹配原则,无疑为编译错误。而且即使s2中的字串长度为4(char s2[]="best"),通过了模板类型参数匹配这一关,使模板类型参数char[5]匹配类型形参T,但还是不能通过对第7行“temp = a;”定义语句的编译,它等价于"char temp[5] = a;",因此导致一个数组初始化非法的编译错误
2.数据形参
数据形参的类型分两种,一种是引有型参数,就像swap模板的"T&a,T&b",另一种是非引用型参数。例如,下面max函数模板中的"T a,T b"
1.template<typename T>
2.T max(T a,T b)
3.{
4. return a<b?b:a;
5.}
引用型参数又分两种,一种是引用型参数,在函数执行过程中,其数据形参的改变会波及到数据实参的改变。另一种是常量引用型参数,即在引用参数前加上const,其数据形参值不允许发生改变,因而不会改变数据实参的值。例如,上面的max函数模板代码可写成下面更好的形式
template<typename T>
const T max(T const& a,T const& b) //常量引用型参数
{
return a<b?b:a;
}
注意,"const T& a" 与"T const& a"等价
3.常量引有型形参
对于常量引用型参数,可以通过显式模板类型指定来规定调用的代码。对于调用中的几个数据实参类型不同,而数据形参类型却要求相同时,用显式模板类型指定的方法是必要的,否则模板参数将拒绝匹配,例如:
/////////03.cpp
1.#include<iostream>
2.template<typename T>
3.T const& max(T const& a,T const& b) //常量引用型类型
4.{
5. return a<b?b:a;
6.}
7.int main()
8.{
9. int ia = 3;
10. double db=5.0;
11. std::cout<<max<double>(ia,db)<<"\n";
12. std::cout<<max(static_cast<double>(ia),db)<<"\n";
13.}
输出:6.7
6.7
显式模板类型指定可以显式指定模板的类型实参,从而也就规定了数据形参的类型,免去了数据实参的演绎,同时也给出了模板函数名。例如,上面的11行的max<double>,实际上确定了模板函数名,使其成为普通函数,因而服从普通函数的匹配规则,从而让int隐式转换为double.显然前提是int能够被隐式转换成double,如果是下面这样的调用,再怎么显式指定模板类型实参,也通不过编译,因为指针&ia不能隐式转换成double:max<double>(&ia,db); //错
当然还可以预先将数据实参转换成预料的数据实参演绎所需要的类型。例如,程序中的12行,先将ia明确地转换为double,从而从数据实参类型符合匹配数据形参类型条件,再根据数据形参类型到类型形参的演绎,获得模板类型匹配,从而获得真正的模板函数及其调用 。
4.引用型形参
对于引用型形参,由于要求数据形参与数据实参的捆绑(互为别名),访问数据形参就是访问数据实参,因此要求数据实参应为左值表达式,不能是常量或字面值。例:
//////04.cpp
1.#include<iostream>
2.template<typename T>
3.void swap(T& a,T& b) //常量引用型类型
4.{
5. T temp = a;a = b;b = temp;
6.}
7.int main()
8.{
9. int ia = 3;
10. const int cb=5;
11. swap(ia,7); //错
12. swap<int>(ia,7); //错
13. swap(cb,7); //错:以const int 匹配
14. swap<const int>(ia,7); //同第13行
15.}
因为语句"int& b = 7;"和"int& b= cb;"在标准C++中是不被接受的。(因为cb是一个常量,所以我们不能直接用一个引用来引用cb,因为我们不能修改cb)所以对于11-12行语句,不能进行数据实参到数据形参的引用初始化,或者说,不能进行数据实参类型到数据形参类型的匹配,即使显式指定模板类型实参(12行),也不能通过编译。而13-14行语句虽能通过编译,但它是以const int 的模板类型实参匹配T的,不能进行引用型形参的写操作。因而,第5行数据形参a和都是const int&型而做违法的赋值操作,终使编译无法进行下去。
5.函数模板重载
有些函数模板,适合用常量引用型形参,而有些函数模板,适合用引用型形参。在max模板中,其数据形参进行了operator<运算,但并不是一切数据类型都拥有operator<运算的。当max模板所接受的类型不具有operator<操作时,该类型的对象就不适合调用max模板。于是就得重载max函数或max模板。
例如,求两个C字符串的较大串。我们知道C字符串的大小比较,一般是通过库函数调用完成的。若根据03.cpp中的max模板,调用max("hello","good"),则此时,hello和good的类型便演绎成char*,也就是指针a指向第一个C串,指针指向第二个C串,这时候的比较,是两个指针在比较,不能反映字串比较的真实大小。
为了安全的用好通用模板,又照顾到某种特殊用法,比较折衷的方法就是要为特殊类型重载max.即,对于数据实参为C串的情况,另外重载一个函数,专门实现没有"<"比较的max版本。该比较操作可以调用库函数strcmp来完成,其max重载函数代码如下:
const char* const& max(const char* const& a,const char* const& b)
{
return std::strcmp(a,b)<0>b:a;
}
该max函数不是模板。在既有03.cpp中的max函数模板,又有对付特殊使用的重载代码的上下文环境中,"max("hello","good");"函数调用,将面临匹配选择的境地。
模板机制规定,如果一个调用既匹配普通函数,又匹配模板函数,刚先匹配普通函数。所以max("hello","good")将匹配上面的普通函数。
接下去的问题是,如果是任何指针所指向的实体的max操作,那就需要间接访问之后再进行比较。因此,在下面的调用中:
void f(int* ix,int* iy,double* dx,double* dy)
{
int* ip = max(ix,iy);
double* dp = max(dx,dy);
}
对于03.cpp中的max模板(第3-6行),本实例分别将int*匹配T或者double*匹配T,根据模板定义体的操作规定,做了指针比较ix<iy以及dx<dy的操作。但事实上,max调用的本意是要进行指向实体的比较。为了真实反映要求,需要对所有的指针实参,先做间访操作,然后进行比较。为此,对所有指针参数类型的max调用进行重载。显然,这是可以有模板表示的一类函数,其重载为模板重载,其数据实参表现为常量引用型参数,返回类型为指针。
template<typename T>
T* const& max(T* const& a,T* const& b)
{
return *a<*b?b:a;
}
这是名为max的另一类函数模板,与前面的03.cpp中的max模板同名,因此构成了函数模板的重载。对于函数调用中的指针数据实参,刚首先匹配此类模板。下列程序在不同的调用需求中,重载的函数和模板将各司其职:
05.cpp
1.#include<iostream>
2.template<typename T>
3.T const& max(T const& a,T const& b)
4.{
5. return a<b?b:a;
6.}
7.template<typename T>
8.T* const& max(T* const& a,T* const& b)
9.{
10. return *a<*b?b:a;
11.}
12.const char* const& max(const char* const& a,const char* const& b)
13.{
14. return std::strcmp(a,b)<0?b:a;
15.}
16.int main()
17.{
18. int ia = 3,ib = 7;
19 char* s1="hello";
20. char* s2="hell";
21. std::cout<<*max(&ia,&ib)<<"\n"; //匹配于第二个模板
22. std::cout<<max(s1,s2)<<"\n"; //匹配于max函数
23. std::cout<<max(ia,ib)<<"\n"; //匹配于第一个模板
24.}
输出:7
hello
7
如果匹配普通函数,刚两个函数模板都没有话说,只能退缩一侧,而让普通函数先行。例如,第23行的max(s1,s2)其实也匹配第一个函数模板的,但因为它也匹配普通函数,所以优先匹配普通函数。如果不匹配普通函数,刚视其数据实参的类型来选择两个重载模板中的一个。显然,第21行的调用中,数据实参类型为指针,又不能匹配普通函数,所以毫不犹豫地匹配了第一个max函数模板。