函数模板
(1)什么是函数模板
函数模板就是一个函数的模板,就像一个模具,你可以用不同的颜料,材料填充它,但是不管选的什么材料,什么颜料,它的形状是不变的。这个模具就是函数的模板。那么很清楚了,如果函数的功能一样(模具),但是参数不一样(颜料),那么就可以使用函数模板,换一个参数即可。下面再说。
(2)使用函数模板
函数模板允许以任意类型的方式来定义函数。例如:
template<typename Anytype> //template是模板关键字,这里class也可以作为关键字,和typename等价。
void Swap(Anytype &a, Anytype &b)
{
Anytype temp;
temp=a;
a=b;
b=temp;
}
第一行指出,要建立一个模板,并将类型命名为Anytype。关键字template和typename是必需的,除非可以使用关键字class代替typename。类型名可以自己选择,这里是”Anytype“。
模板不创建函数,只是告诉编译器如何定义函数。
下面给一个例子来说明函数模板的使用:
#include<iostream>
using namespace std;
//template<typename T> //函数模板格式
//void Swap(T& a, T& b);
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, T& c)
{
T temp;
temp = a;
a = b;
b = temp;
temp = c;
c = b;
b = temp;
}
int main()
{
int a, b;
int e = 4;
a = 2, b = 3;
cout << "a=" << a << " b=" << b << endl<<"Swap\n";
Swap(a, b);
cout << "a=" << a << " b=" << b << endl<<endl;
double c = 2.4;
double d = 5.0;
cout << "c=" << c << " d=" << d << endl<<"Swap\n";
Swap(c, d);
cout << "c=" << c << " d=" << d << endl<<endl;
cout << "a=" << a << " b=" << b << " e=" << e << endl << "Swap\n";
Swap(a, b, e);
cout << "a=" << a << " b=" << b << " e=" << e << endl;
return 0;
}
从上面的程序发现函数模板的参数是泛型,而其重载的方式和普通函数重载方式一样,值得一提的是,函数模板参数列表可以有具体类型而不是泛型,比如:
templete<typename T>
void swap(T &a,T &b,int c); //c不是泛型而是具体类型
可见函数模板虽然大部分都是以泛型作为其参数列表以获得更好的通用性,但是仍然可以有具体类型的参数,这样使函数模板更具有兼容性和多样性。
(3)模板的局限
首先我们知道模板函数的参数大多数是泛型,那么这就产生了问题。泛型类型的变量,怎么说,有所局限。比如:
template<class T>
void f(T a,T b)
{
if(a>b)
return a;
return b;
}
上面函数f看起来没有什么问题,但是试想,如果实参是数组,那么会如何?
很显然不行的,因为C++不支持数组直接的比较。
再来看一个:
struct job
{
char name[40];
double salary;
int floor;
};
template<typename T> //函数模板
void Swap(T& a, T& b)
{
T temp;
temp = a;
a = b;
b = temp;
}
上面定义一个结构体,如果想要交换两个结构体变量,用Swap函数可以实现。C++允许将一个结构赋给另一个结构。这没有问题。但是,如果只想交换结构体里面的一个成员变量呢?那么此时Swap就不能满足这个功能了。
从上面两个例子可以看出,模板函数还是很有局限性的。
(4)显示具体化
那么这种局限,通过模板函数重载常常也解决不了,那么怎么办?
- 通过非模板函数的函数重载实现需要的功能
- 显示具体化
第一个解决方案很好理解,就是新建一个常规函数来实现需要的功能。那么第二个是什么呢?
显示具体化:
- 显示具体化的原型和定义以template<>打头,并举出具体类型
- 具体化优先于常规模板,而非模板函数优先于具体化和常规模板
下面是用于交换job结构的非模板函数,模板函数,具体化原型
void Swap(job &,job &); //非模板函数
template<typename T>
void Swap(T &,T &); //常规模板
template<>void Swap<job>(job &,job &); //显示具体化
Swap< job >里面的< job>可写可不写。
下面给一个程序好好了解具体化怎么工作:
#include<iostream>
using namespace std;
template<typename T>
void Swap(T& a, T& b) //常规模板,优先级最低
{
T temp;
temp = a;
a = b;
b = temp;
}
struct job
{
char name[40];
double salary;
int floor;
};
template<>void Swap<job>(job& j1, job& j2) //显示具体化,和之前的常规模板区别明显,一个是泛式类型T,一个是具体类型job
{
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)
{
cout << j.name << " : $" << j.salary
<< " on floor" << j.floor << endl;
}
int main()
{
cout.precision(2);
cout.setf(ios::fixed, ios::floatfield);
int i = 10, j = 21;
cout << "i,j=" << i << ", " << j << ". \n";
cout << "Using compiler-generated int swapper:\n";
Swap(i, j); //使用常规模板
cout << "Mow i,j = " << i << " ," << j << ". \n";
job sue = { "lby",10000000, 100};
job sidney = { "liutie",222123,101 };
cout << "Before job swapping :\n";
show(sue);
show(sidney);
Swap(sue, sidney); //使用具体化模板
cout << "After job swapping:\n";
show(sue);
show(sidney);
return 0;
}
(5)实例化和具体化
代码中包含函数模板本身不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。模板不是函数定义,而具体了参数的模板实例是函数定义。这种实例化方式被称为隐式实例化
既然有隐式实例化,那就有显示实例化。
首先,上面说到,函数模板本身不会生成函数定义,编译器将函数模板看作一个生成方案,具体用到的时候临时生成函数定义。那么,使用显示具体化后,直接命令编译器创建特定的示例。
下面看看显示具体化和显示实例化的区别:
显示具体化:
template<>void Swap<int>(int &,int &); //等价于下面,显示具体化
template<>void Swap(int &,int &);
上面两个等价的声明都是显示具体化,意思是“不要使用Swap()模板来生成函数定义,而应该使用专门为int类型显式定义的函数定义”
显示实例化:
template<class T> //常规模板
void swap(T &,T&);
template<>void swap<job>(job &,job &); //显示具体化
template void swap<char>(char &,char &); //显示实例化
说说人话吧:显示实例化就是对模板函数的一个实例,只不过先写好了而已。实际上隐式实例化也可以做到一样的事情。而显示具体化就完全不一样了,这个家伙除了喜欢利用名字把你绕晕,他和显示实例化和隐式实例化没有关系。显示实例化是一个新的函数定义。
(6)编译器如何选择使用哪个函数版本
既然一个函数有这么多版本:函数重载,函数模板,函数模板重载,那么遇到的时候难免会产生混乱,不知道用哪个。但是C++会有一套解决措施—-重载解析
下面是步骤:
- 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例
- 确定是否有最佳的可行函数,如果有就使用,否则报错。
考虑只有一个函数参数的情况,如下面的调用:
may('B');
首先,编译器将寻找候选者,即名称为may()的函数和函数模板。然后寻找那些可以用一个参数调用的函数。例如,下面的函数符号要求,因为其名称与被调用的函数相同,且可只给它们传递一个参数:
void may(int); //#1
float may(float ,float=3); //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)不可选,因为整数类型('B’虽然是char类型,同时也可以看作int型的ascll码)不可被隐式的转换为指针类型。而#6的模板函数可以生成具体化(隐式实例化)(隐式实例化,显示实例化,显示具体化都叫具体化),其中T被转换为char类型。这样一来,排除了两个函数,还有5个函数在候选队列,怎么搞?选哪个?
接下来,编译器必须确定哪个是可行函数是最佳的。它查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。通常,从最佳到最差的顺序如下所述:
- 完全匹配,但常规函数优先于模板
- 提升转换(char到int,float到double)。
- 标准转换(int到char,long到double)。
- 用户定义的转换,如类声明中的转换
例如:函数#1优于函数#2,因为char到int使提升转换,而char到float是标准转换。而提升转换的优先级高于标准转换,所以如果只有#1,#2两个函数,则优先调用函数#1。函数#3,#5,#6都是完全匹配(但是#3和#5又优于#6(常规函数优于模板)),它们都优于#1,#2。所以如果这5个函数都存在,则不考虑#1,#2,#6。这样一来,就只剩下#3和#5了。
通常,有两个函数完全匹配是错误的,但是有2个例外。
1.完全匹配和最佳匹配
进行完全匹配的时候,C++允许某些“无关紧要的转换”。
从实参 | 到形参 |
---|---|
Type | Type & |
Type & | Type |
Type | const Type |
Type | volatile Type |
Type[] | *Type |
Type * | cosnt Type |
假设有下面的函数代码:
struct bolt{int a;char b[10];};
blot ink={25,"sport"};
....
recycle(ink) //ink是实参
在这种情况下,下面的原型都是完全匹配的:
void recycle(blot); //完全匹配 #1
void recycle(const blot); // #2 blot到const blot是“无关紧要的”,也看成完全匹配
void recycle(blot &); // #3 blot到blot&是无关紧要的,也看成完全匹配(只有满足“无关紧要”的条件,都是完全匹配)
void recycle(const blot &); //#4
没错,如果有多个匹配的原型,则编译器将无法完成重载解析的过程;如果没有最佳的可行函数,则编译器将生成一条错误信息,该消息可能会使用诸如“ambiguous”二义性这样的词语。
然而有时候即使两个函数都完全匹配,也可以完成重载解析。首先,指向非const的数据的指针和引用优先与非const指针和引用参数匹配。也就是说,在recycle()里,如果只定义了#3,#4,则选择#3.然而,const与非const之间的区别只适用于指针和引用指向的数据。也就是说,如果只定义了#1和#2,将出现二义性错误。
一个完全匹配优于另一个的是:其中一个是非模板函数,一个是模板函数。非模板的优先级高。
如果两个完全匹配都是模板函数,则较具体的模板函数优先。例如,这意味着显示具体化将优于使用模板隐式化生成的具体化。:
struct blot{int a;char b[10];};
template<class Type>void recycle(Type t); //模板函数
template<>void recycle<blot>(blot &t); //显示具体化(重新定义模板)
...
blot ink={25,"sports"};
...
recycle(ink); //这里有两种选择,一种是模板隐式生成的具体化,一种是显示具体化,优先使用显示具体化。
术语“最具体”不一定意味着显示具体化,而是指编译器推断使用哪种类型时执行的转换次数最少。例如:
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,"sports"};
...
recycle(&ink);
recycle(&ink)与#1匹配,匹配时将Type解释为blot*。recycle(&ink)也与#2匹配,Type被解释为blot。因此两个隐式实例—recycle<blot*>(blot*)和recycle< blot>(blot*)被发送到可行函数池。
在这两个模板函数中,recycle(blot*)被认为更具体,因为在生成过程中,他需要的转换更少。
用于找出最具体的模板的规则被称为函数模板的部分排序规则。和显示实例一样,时C++98新增的特性!
2.部分排序规则:
先看一个程序,它使用部分排序规则来确定要使用哪个模板定义。
#include<iostream>
using namespace std;
template<typename T> //模板A
void showArray(T arr[], int n)
{
cout << "template A\n";
for (int i = 0; i < n; i++)
cout << arr[i] << " ";
cout << endl;
}
template<typename T> //模板B
void showArray(T* arr[], int n)
{
cout << "template B\n";
for (int i = 0; i < n; i++)
cout << *arr[i] << " ";
cout << endl;
}
struct debts
{
char name[50];
double amount;
};
int things[6] = { 13,31,103,301,310,130 };
struct debts mr_E[3] =
{
{"Ima Wolfe",2400.0},
{"Ura Foxe",1300.0},
{"Iby Stout",1800.0}
};
int main()
{
double* pd[3];
for (int i = 0; i < 3; i++)
pd[i] = &mr_E[i].amount;
cout << "Listing Mr.E's counts of things:\n";
showArray(things, 6);
cout << "Listing Mr.E's debts:\n";
showArray(pd, 3);
return 0;
}
见上述程序。
showArray(things,6);
由于things是一个int数组名,会匹配下面的模板;
template<typename T> //模板A
void showArray(T arr[], int n)
{
cout << "template A\n";
for (int i = 0; i < n; i++)
cout << arr[i] << " ";
cout << endl;
}
其中T被替换为int类型
接下来,看下面的函数调用
showArray(pd,3);
其中pd是一个double*类型的名称,这与模板A匹配:
template<typename T> //模板A
void showArray(T arr[], int n)
{
cout << "template A\n";
for (int i = 0; i < n; i++)
cout << arr[i] << " ";
cout << endl;
}
其中,T被替换为类型double *。在这个情况下,模板函数将显示pd数组的内容,即三个地址。恰巧该函数也与模板函数B匹配:
template<typename T> //模板B
void showArray(T* arr[], int n)
{
cout << "template B\n";
for (int i = 0; i < n; i++)
cout << *arr[i] << " ";
cout << endl;
}
这里T被替换为类型double。而函数将显示被解除引用的元素*arr[i],即数组内容指向的double值。
在这两个模板里,模板B更具体,因为它做出了特点的假设—数组内容是指针,因此被使用。(也可以说,实参类型传递过来没有做进一步的转换,所以更加具体)
总结
简而言之,重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果有多个,但其中只有一个是非模板的,则选择它;如果存在多个合适函数,且都为模板函数,但其中有一个比其它更具体,则选择它。如果有多个同样合适的非模板函数或模板函数,但没有一个是最具体的,则函数调用出现错误。