C++的函数模板是通用的函数描述。怎么理解呢?通俗地讲,通过使用泛型来定义函数,泛型指的是其可用具体的类型(int/double)来替换,通过将类型作为参数传递给模版,可以使编译器生成该类型的函数。
当然由于模版允许以泛型的方式来编写程序,因此有时也被称为通用编程。
函数模版定义
我们通过一个简单的例子来看看函数模版的存在的意义:
假设我们有一个函数来交换两个int类型的值:
void swap_or(int &a,int &b){
int temp;
temp=a;
a=b;
b=temp;
}
原始的函数是这样,但是现在有个问题:假设说我们现在要交换两个double类型的值那么这个函数就不行了,但是我们可以复制原来的代码用int来替换,当然如果需要交换两个char类型的我们以可以同样在写一个函数,但是这样会浪费时间,并且容易出错,为了避免这样的情况出现,C++的函数模版,可以自己完成这个过程,更简单也更可靠。
//定义
template <typename AnyType>
/**
* C++98标准库也可以使用class来创建模版
*
* template <class AnyType>
* class 和template是等价的
*/
void Swap(AnyType &a,AnyType &b){
AnyType temp;
temp =a;
a=b;
b=temp;
}
第一行我们要支出,建立一个模版,并且类型明明为AnyType当然为了书写方便我们通常用T来表示,关键字 template 和 typename是必须的必须使用尖括号<>。
注意:模版并不创建任何函数,只是告诉编译器如何定义函数。需要交换int类型的函数时,编译器将按照模版模式传见一个int类型参数的函数,同样double也是如此。
代码中的注释还标注了一点,就是typename可以用class来替换,但是我们现在通常推荐使用typename来些,class是老版本的标准。
接下来我们测试一下这个函数模版:
//声明
template <typename AnyType>
void Swap(AnyType &a,AnyType &b);
//在main函数中测试
int main(){
int a =10;
int b =11;
cout<<"交换前a="<<a<<",b="<<b<<endl;
Swap(a,b);//编译器根据模版函数生成了一个Swap函数接受两个int类型参数
cout<<"交换后a="<<a<<",b="<<b<<endl;
float c = 3.14;
float d = 3.16;
cout<<"交换前c="<<c<<",d="<<d<<endl;
//隐式实例化出int类型的一个模版实例
Swap(c,d);//编译器根据模版函数生成了一个Swap函数接受两个float类型参数
//现在还支持显式实例化
//比如template void Swap<int>(int &,int &);
cout<<"交换后c="<<c<<",d="<<d<<endl;
//显式具体化是需要在template后面加上尖括号<>。
//template <> void Swap<int>(int &,int &);
return 0;
}
//定义
template <typename AnyType>
void Swap(AnyType &a,AnyType &b){
AnyType temp;
temp =a;
a=b;
b=temp;
}
模版定义在main函数后面,所以我们先声明函数,执行的结果是:
这里我们就可以看到了只有一个函数模版,就能够同时实现int类型和double类型的交换操作,这就解决了上述的那个问题。
但是这里还有一个问题,函数模版让我们少写了一个函数,是不是也让我们缩短了可执行的程序呢?这里需要注意了:
函数模版并不能缩短可执行程序,最终仍然是由两个独立的函数定义,最终的代码不包含任何模版,只包含了为程序生成的实际函数,也就是说这个例子里,最终还是两个函数,一个参数是int类型,一个参数是double类型。
在实际开发中,更常见的情形是,把模版放在头文件里,谁用谁包含这个头文件。
函数模版的优点
1.提高代码的复用性
2.增强代码的可维护性
3.支持泛型编程(强调代码的通用性和可重用性不依赖于具体的数据类型)
但是,函数模版并不能缩短可执行程序,也就是说并不能缩短程序的运行时间。
重载的模版
需要对多个不同类型使用同一算法的函数时,可以使用模版,然而并不是所有的类型都是用相同的算法,为了满足这种需求,可以像常规函数定义那样去重载模版函数,和常规重载一样,参数需要不同,举个例子来看:
template <typename T>
void Swap(T &a,T &b);
template <typename T>
void Swap(T *a,T *b,int n);
void Show(int a[]);
const int Lim =8;
int main(){
using namespace std;
int i=10,j=20;
cout<<"original i="<<i<<",j="<<j<<endl;
Swap(i,j);
cout<<"now i="<<i<<",j="<<j<<endl;
int d1[Lim]={0,7,0,4,1,7,7,6};
int d2[Lim]={0,7,2,0,1,9,6,9};
cout<<"original arrays "<<endl;
Show(d1);
Show(d2);
Swap(d1,d2,Lim);
cout<<"swapped arrays "<<endl;
Show(d1);
Show(d2);
return 0;
}
template <typename T>
void Swap(T &a,T &b){
T temp;
temp=a;
a=b;
b=temp;
}
template <typename T>
void Swap(T a[],T b[],int n){
T temp;
for (int i = 0; i < n; ++i) {
temp=a[i];
a[i]=b[i];
b[i]=temp;
}
}
void Show(int a[]){
using namespace std;
cout<<a[0]<<a[1]<<"/";
cout<<a[2]<<a[3]<<"/";
for (int i = 4; i <Lim ; ++i) {
cout<<a[i];
}
cout<<endl;
}
输出结果是:
这里我们可以看到
template <typename T>
void Swap(T a[],T b[],int n)
其实是
template <typename T>
void Swap(T &a,T &b)
的重载,参数不同,执行的逻辑也不同,这里通过例子还能够看出来并非所有的模版参数都必须是模版参数类型,也就是说参数列表的参数类型并不一定都得是T,我们看到重载的模版里面也有int类型。
模版的局限性
假设有如下模版
template <class T>
void f(T a, T b){...}
如果代码假定定义了赋值,a=b;但如果T是数组,这种假设就不成立,同样如果假设是a>b但如果T为结构,结社也不成立,总之编写的模版函数很可能无法处理某些类型。另外一方面,有时候通用化是有意义的也有必要的,但是C++语法并不支持,例如,将两个包含位置坐标的结构相加是有意义的,一种解决方法是重载运算符+,以便能够将其用于特定的结构或类,另一种解决方案是:为特定类型提供具体化的模版定义。
显示具体化
假设我们有这个结构体:
struct job
{
char name[40];
double salary;
int floor;
};
希望能够交换两个这种结构的内容,原来的模版使用下面的代码完成交换:
temp=a;
a=b;
b=temp;
C++是允许将一个结构赋给另一个结构,因此即使T是一个结构,上述代码也适用,然而假设只想交换salary和floor成员,而不交换name成员,则需要使用不同的代码,由于参数没有变化也无法使用模版重载,这个时候该怎么办呢?可以提供一个具体化函数定义-称之为显示具体化(explicit specialization)
void Swap(job &,job &);//非模版函数
template <typename T>
void Swap(T &,T &);//模版函数
template <> void Swap<job>(job &,job &);//显示具体化原型
其中
template <> void Swap<job>(job &,job &);
即为显示具体化的原型。
当然这里引入了一个问题,就是编译器在选择原型的时候,非模版版本的函数优先于显示具体化和模版版本,二显示具体化优先于使用模版生成的版本。
显示具体化示例
#include "iostream"
struct job
{
char name[40];
double salary;
int floor;
};
//void Swap(job &,job &);
template <typename T>
void Swap(T &,T &);
//explicit specialization
template <> void Swap<job>(job &,job &);
void Show(job &j);
int main(){
using namespace std;
cout.precision(2);
cout.setf(ios::fixed,ios::floatfield);
int i= 10,j=20;
cout<<"i,j="<<i<<","<<j<<".\n";
cout<<"using compiler-generated int swapper\n";
Swap(i,j);
cout<<"now i,j="<<i<<","<<j<<".\n";
job sue={"Susan yaffee",73000.60,7};
job sidney={"Sidney Taffee",78060.72,9};
cout<<"Before job swapping:\n";
Show(sue);
Show(sidney);
Swap(sue,sidney);
cout<<"After job swapping\n";
Show(sue);
Show(sidney);
return 0;
}
template <typename T>
void Swap(T &a,T &b){
T temp;
temp=a;
a=b;
b=temp;
}
template<> void Swap<job>(job &j1,job &j2){
double t1;
int t2;
t1=j1.salary;
j1.salary=j2.salary;
j2.salary=t1;
t2=j1.floor;
j1.floor=j2.floor;
j2.floor=t2;
}
void Show(job &j){
using namespace std;
cout<<j.name<<":$"<<j.salary<<" on floor "<< j.floor<<endl;
}
输出结果:
实际上我们这里显式具体化的意思通俗地理解就是为特定的场景,编写特定的代码,只不过还是利用了同一个函数模版,当检测到独特的参数列表时,直接去调用显式实例化这个版本的函数。
实例化和具体化
为了进一步理解模版,需要明白实例化和具体化,当编译器使用模版为特定类型生成函数定义时,得到的是模版实例(instantiation),例如前面例子的Swap(a,b),编译器生成了一个Swap()的实例,该实例使用int类型,模版并不是函数定义,但是使用int的模版实例是函数定义。这种实例化方式被称为隐式实例化(implicit instantiation),因为编译器之所以知道需要进行定义,是由于程序调用了Swap()函数时提供了int参数。
最初编译器只能通过隐式实例化来使用模版生成实例,但是现在C++还允许显式实例化(explicit instantiation)语法时:声明所需的种类-用<>符号只是类型,并在声明前加上关键字template:
template void Swap<int>(int,int) ;
此时编译器看到上述声明时,将使用Swap模版生成一个使用int类型的实例,也就是说生成了一个函数定义。
显式具体化的声明语法是:
template <> void Swap<int>(int &,int&) ;
template <> void Swap(int &,int&) ;
也就是在template后面多了一对尖括号<>,意思表明了“不要使用Swap模版来生成函数定义,而应该使用专门为int类型显式地定义函数定义”,这些原型必须要有自己的函数定义。
所以这里就会出现一个警告:如果在同一个文件中使用同一种类型的显式实例和显示具体化将出错。
template <> void Swap<int>(int &,int &);//具体化
template void Swap<int>(int &,int&);//实例化
报错:
两个都是int类型的Swap实例化和具体化发生冲突。
上面我们提到的一些名称如隐式实例化、显式实例化、显示具体化统称为具体化,相同之处在于表示的都是使用具体类型的函数定义,而不是通用描述。
但我上面说了这么多,也就意味着,一个模版可以关联多个函数,那么就有了一个问题,编译器如何选择哪个函数版本呢?
编译器确定版本
对于函数重载、函数模版、函数模版重载,C++需要一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是多个参数时,这个过程称为重载解析(overloading resolution)。
重载解析的过程步骤:
第一步:创建候选函数列表,包含与被调用函数的名称相同的函数和模版函数。
第二步:使用候选函数列表创建可行函数列表,这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况,例如使用float参数的函数调用可以讲改参数转换为double,从而与double形参匹配,而模版可以为float生成一个实例。
第三步:确定是否有最佳的可行函数,如果用则使用,否则函数调用出错。
那我们还是举一个例子看看编译器到底是如何确定调用哪个函数的。
这里有个调用:
may('B');
首先编译器寻找候选者,即名称为may()的函数和函数模版,然后寻找那些可以用一个参数调用的函数,如下列函数符合要求,
void may(int);//#1
float may(float,float=3);//#2
void may(char);//#3
char * may(const char *);//#4
char may(const char &);//#5
template<class T> void may(const T&);#6
template<class T> void may(T*);#7
我们只考虑参数不考虑返回值,那么#4和#7可以先被排除,因为整数类型不能够被隐式的转换为指针类型,这样剩下5个可行的函数,其中每个函数,如果它是声明的唯一一个函数,都可以被使用,那么接下来编译器就要确定,哪个函数是最佳的。
通常,从最佳到最差的顺序如下:
1.完全匹配,但常规函数优于模版函数。
2.提升转换(例如char和shorts自动转换为int,float自动转换为double)
3.标准转换(例如int转换为char,long转换为double)
4.用户定义的转换,如类声明中定义的转换。
那么根据这些个规则,#1优于#2因为提升转换优于标准转化,函数#3,函数#5,函数#6优于#1、#2,因为他们都是完全匹配的,而#3、#5又是优于#6的因为#6函数是模版,那么以上的分析又引出了两个问题,什么是完全匹配?如果两个函数#3和#5都完全匹配应该怎么办呢?通常来讲有两个函数完全匹配是一种错误,但是有两个例外。
1.完全匹配和最佳匹配
进行完全匹配时,C++允许某些“无关紧要的转换”。比如int实参和int&形参完全匹配。int[]实参与*int形参也完全匹配,int实参与const int形参也是完全匹配的。
以下是一些无关紧要的转换列表:
那么正如预期的如果
如果有多个匹配的原型,则会出现二义性的错误,但是有时候即使两个函数都完全匹配,仍然可以完成重载解析,首先第一种:指向非const数据的指针和引用优先与非const指针和引用参数匹配。
另一种一个完全匹配优于另一个完全匹配的情况是,其中一个是非模版函数,另一个不是,这种情况下,非模版函数则将优于模版函数,如果两个都是模版函数,则较具体的模版函数优先,这意味着显示具体化则优于模版隐式生成的具体化。
“最具体”这里并不一定是说显示具体化,而是指编译器推断使用哪种类型时执行的转换最少。
如下例子:
template <class Type> void recycle(Type t);//#1
template <class Type> void recycle(Type * t);//#2
struct blot{int a;char b[10];};
blot ink = {25,"spots"};
..
recycle(&ink)
在这个例子里,与模版#1匹配,将Type解释为blot*
和模版#2都匹配,将Type被解释为blot,因此将两个隐式实例-recycle<blot*>(blot*)和recycle<blot>(blot*)发送到可行函数池子中,这两个模版函数中,
recycle<blot*>(blot*)被认为是更具体的,因为在生成过程中,它需要进行的转换更少,也就是说#2模版已经显式的指出,函数参数是指向type的指针,因此可以直接用blot表示type,而#1的模版将type作为函数参数,因此type必须被解释为只想blot的指针。
同样的我们举个例子:
template <typename T>
void ShowArray(T arr[],int n);//template A
template <typename T>
void ShowArray(T* arr[],int n);//template B
struct debts
{
char name[50];
double amount;
};
int main(){
using namespace std;
int things[6] = {13,31,103,301,310,130};
struct debts mr_E[3]=
{
{"IMA",2400.0},
{"IMB",1300.2},
{"IBY",1800.5}
};
double * pd[3];//指针数组,存放double类型指针的数组
for (int i = 0; i < 3; ++i) {
pd[i]=&mr_E[i].amount;
}
ShowArray(things,6);
ShowArray(pd,3);
return 0;
}
template <typename T>
void ShowArray(T arr[],int n){
using namespace std;
cout<<"Template A"<<endl;
for (int i = 0; i < n; ++i) {
cout<< arr[i]<<' ';
}
cout<<endl;
}
template <typename T>
void ShowArray(T * arr[],int n){
using namespace std;
cout<<"Template B"<<endl;
for (int i = 0; i < n; ++i) {
cout<< *arr[i]<<' ';
}
cout<<endl;
}
运行结果:
根据例子中的函数调用:
ShowArray(things,6);
things是一个int数组的名称,因此与模版A匹配,
其中T被替换为int类型。
而
ShowArray(pd,3);
其中pd是一个double* 数组的名称,这与模版A匹配,
其中T被替换为double* 这种情况下,模版函数将显示pd数组的内容,即三个地址,但是该函数调用同样与模版B匹配,此时T被替换为类型double,而函数将显示被解引用的元素*arr[i],即数组内容指向的double值,在这两个模版中,B更具体,因为它做了特定的假设:数组的内容是指针,因此实际模版B被使用。
如果将模版B从程序中删除,运行程序同样也不会报错,但是pd显示的内容将是地址,而不是值。
decltype关键字
编写模版函数时,并非总能知道应在生命中使用哪种类型,比如两个数相加,一个是double一个是int,这种情况下结果应该是double类型,如果一个是int一个是short这种情况下两个结果应该是int类型,那如果是short和char呢?结构和类呢?这将会导致问题更加复杂。
C++11中新增了关键字decltype提供解决方案。
比如:
decltype(x+y) xpy;
xpy=x+y;
使得xpy跟x+y的结果类型是一致的。
同样,还有一个位置返回值的问题,我们无法预先将x+y相加得到的类型,好像可以将返回类型设置为decltype(x+y)但是很遗憾的是此时还未声明参数x和y,所以无法使用它们。为此C++新增了一种声明和定义函数的语法,
auto h(int x,float y) ->double
通过结合这种语法和decltype可以给函数指定返回值类型
template<class T1,class T2>
auto gt(T1 x,T2 y)->decltype(x+y){
....
return x+y;
}
那么以上就是函数模版的全部内容了。