函数模板
1.快速上手
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数。
#include<iostream>
using namespace std;
template <typename T>
void Swap(T &a,T &b);//模板原型
struct apple{
string name;
double weight;
int group;
};
void show(apple x);
int main(){
int a,b;
a=1;
b=2;
Swap(a,b);
cout<<"a:"<<a<<endl;
cout<<"b:"<<b<<endl;
apple c={"Alice",200,1};
apple d={"Bob",250,2};
Swap(c,d);
cout<<"c:"<<endl;
show(c);
cout<<"d:"<<endl;
show(d);
}
template <typename T>
void Swap(T &a,T &b){
T temp;
temp=a;
a=b;
b=temp;
}
void show(apple x){
cout<<"name:"<<x.name<<endl;
cout<<"weight:"<<x.weight<<endl;
cout<<"group:"<<x.group<<endl;
}
a:2
b:1
c:
name:Bob
weight:250
group:2
d:
name:Alice
weight:200
group:1
模板函数也可以有原型: template <typename T> void Swap(T &a,T &b); 这里的typename 也可以换成class 。 不过模板原型实际上不常见。
模板函数定义:
template <typename T>
void Swap(T &a,T &b){
T temp;
temp=a;
a=b;
b=temp;
}
模板函数隐式实例化: Swap(a,b); 模板函数会根据实参的类型,给出函数定义。 还有显式实例化: Swap<int>(a,b); 显式的定义typename。 对于这两种实例化,我推荐使用显式实例化,因为隐式实例化容易出错。对于这块知识的详细解读,需要有对编译器有充分的理解,在文章后面会给出。
一般我们不会用到模板函数的原型,因为我们一般把模板函数的定义放在头文件里面,再需要使用的时候,包含头文件就行了。 不推荐的做法:模板原型放在头文件,模板定义放在cpp文件里。
2.重载的模板
如果对函数的重载不了解,可以翻看我之前的文章: 从C到C++___内联函数、引用变量、函数重载
模板函数也可以重载,语法和常规函数的重载差不多;被重载的模板函数必须要特征标不同。
#include<iostream>
using namespace std;
template <typename T>
void Swap(T &a,T &b);//模板原型
template <typename T>
void Swap(T *a,T *b,int n);//模板原型
struct apple{
string name;
double weight;
int group;
};
void show(apple x);
int main(){
int a,b;
a=1;
b=2;
Swap(a,b);
cout<<"a:"<<a<<endl;
cout<<"b:"<<b<<endl;
apple c={"Alice",200,1};
apple d={"Bob",250,2};
Swap(c,d);
cout<<"c:"<<endl;
show(c);
cout<<"d:"<<endl;
show(d);
char e[10]="hello";
char f[10]="bye!!";
Swap(e,f,10);
cout<<"e:"<<e<<endl;
cout<<"f:"<<f<<endl;
}
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(apple x){
cout<<"name:"<<x.name<<endl;
cout<<"weight:"<<x.weight<<endl;
cout<<"group:"<<x.group<<endl;
}
a:2
b:1
c:
name:Bob
weight:250
group:2
d:
name:Alice
weight:200
group:1
e:bye!!
f:hello
3.模板的局限性
#include<iostream>
using namespace std;
template<class T>
const T& foo(const T &a,const T &b){
if(a>b)return a;
else return b;
}
struct apple{
string name;
double weight;
int group;
};
void show(apple x);
int main(){
apple c={"Alice",200,1};
apple d={"Bob",250,2};
apple max=foo(c,d);
show(max);
}
void show(apple x){
cout<<"name:"<<x.name<<endl;
cout<<"weight:"<<x.weight<<endl;
cout<<"group:"<<x.group<<endl;
}
上面这段代码是出错的,因为T如果是结构体,我们无法对其做>操作。当然解决这个问题的方法也是有的—显式具体化函数。
4.显式具体化函数–一个畸形产物
显式具体化函数的诞生是因为模板对于某些类型的数据,定义得的函数,例如上例中得foo(c,d) 出错,我们就单独对这个类型,写一个特殊的函数。
所以,就是一句话,原先模板不适用于某种类型的数据,我们就单独给这种类型的数据,单独来一个函数定义。
#include<iostream>
using namespace std;
struct apple{
string name;
double weight;
int group;
};
template <typename T>
void Swap(T &a,T &b);//模板原型
template<>
void Swap<apple>(apple &a,apple &b);//显式具体化函数原型,这里<apple>可以省略
void show(apple x);
int main(){
int a,b;
a=1;
b=2;
Swap(a,b);
cout<<"a:"<<a<<endl;
cout<<"b:"<<b<<endl;
apple c={"Alice",200,1};
apple d={"Bob",250,2};
Swap(c,d);
cout<<"c:"<<endl;
show(c);
cout<<"d:"<<endl;
show(d);
}
template <typename T>
void Swap(T &a,T &b){
T temp;
temp=a;
a=b;
b=temp;
}
template<>
void Swap<apple>(apple &a,apple &b){
cout<<"explicit specialization for apple!"<<endl;
int temp;
temp=a.group;
a.group=b.group;
b.group=temp;
}
void show(apple x){
cout<<"name:"<<x.name<<endl;
cout<<"weight:"<<x.weight<<endl;
cout<<"group:"<<x.group<<endl;
}
a:2
b:1
explicit specialization for apple!
c:
name:Alice
weight:200
group:2
d:
name:Bob
weight:250
group:1
可以看出来,我们单独为 结构体apple 搞了个显式具体化函数,目的就是只交换group成员变量。
显式具体化函数和常规模板很类似。
显式具体化函数的原型: template<> void Swap<apple>(apple &a,apple &b); 这里<apple> 可以省略.
显式具体化函数的定义:
template<>
void Swap<apple>(apple &a,apple &b){
cout<<"explicit specialization for apple!"<<endl;
int temp;
temp=a.group;
a.group=b.group;
b.group=temp;
}
实际上这段代码也意味着,显式具体化的优先级高于常规模板。
5.实例化和具体化
- 切记!函数模板本身不会生成函数定义,它只是一个生成函数定义的方案!
编译器使用模板为特定类型生成函数定义时,得到的是模板实例。生成函数定义就是实例化。 实例化有隐式和显式之分。 模板函数隐式实例化: Swap(a,b); 模板函数会根据实参的类型,生成函数定义。 还有显式实例化: Swap<int>(a,b); 直接根据<>中的类型生成函数定义。
下面这段代码显示了 显式实例化的优越性:
#include<iostream>
using namespace std;
template <typename T>
T Add(const T &a,const T &b){
return (a+b);
}
int main(){
int a=5;
double b=6.1;
cout<<Add<double>(a,b)<<endl;
}
如果把Add<double>(a,b) 换成Add(a,b) 会出错,因为a是int类型的,而b是double类型的,这样就无法隐式实例化了。 Add<double>(a,b) 会显式实例化一个函数定义,然后int类型的a,传参给double的引用形参的时候,会产生临时变量,从而完成函数调用。
显式实例化的还有一种形式是: template double Add<double>(const double &,const double &); 不过这个形式已经被淘汰了。
截至2022年7月,现在最新的C++标准是C++20
上一节中我们提到了显式具体化,我们可以发现实例化和显式具体化的相同之处在于,他们都是使用具体类型的函数定义,而不是通用描述。 显式具体化函数是否是模板函数? 我的回答是:显式具体化函数是一个不伦不类的模板函数,你也可以把他看成是一个畸形产物,他是非模板函数和模板函数的杂种。
#include<iostream>
using namespace std;
struct apple{
string name;
double weight;
int group;
};
template<class T>
void Swap(T &a,T &b);//模板原型
template<>
void Swap(apple &a,apple &b);//显示具体化原型
void show(apple x);
int main(){
short a=1;
short b=2;
Swap(a,b);//隐式实例化
cout<<"a:"<<a<<endl<<"b:"<<b<<endl;
apple c={"Alice",200,1};
apple d={"Bob",250,2};
Swap(c,d);//显式具体化
cout<<"c:"<<endl;
show(c);
cout<<"d:"<<endl;
show(d);
double e=1;
double f=2.01;
Swap<double>(e,f);//显式实例化
cout<<"e:"<<e<<endl<<"f:"<<f<<endl;
}
template<class T>
void Swap(T &a,T &b){
T temp;
temp=a;
a=b;
b=temp;
}
template<>
void Swap(apple &a,apple &b){
int temp;
temp=a.group;
a.group=b.group;
b.group=temp;
}
void show(apple x){
cout<<"name:"<<x.name<<endl;
cout<<"weight:"<<x.weight<<endl;
cout<<"group:"<<x.group<<endl;
}
a:2
b:1
c:
name:Alice
weight:200
group:2
d:
name:Bob
weight:250
group:1
e:2.01
f:1
这里问个问题,如果把上面代码中的e变成 int类型会出现问题吗? 会报错,因为实参和函数中引用形参的类型不一样,且此时不是const引用形参,也不会有临时变量产生。如果你不清楚,且看引用变量的语法。 从C到C++___内联函数、引用变量、函数重载
6.重载解析
6.1 概览
对于常规函数,函数重载,函数模板,函数模板重载,编译器需要有一个良好的策略,从一大堆同名函数中选择一个最佳函数定义。这一过程是非常复杂的过程–重载解析。这就是我们这一节要阐述的内容。
重载解析过程:
- step1:创建候选函数列表。其中包含与被调用函数名称相同的函数和模板函数。
- step2:从候选函数列表中筛选可行函数。其中包括参数正确或者隐式转换后参数正确的函数。
- step3:确定是否存在最佳的可行函数。如果有则使用他,否则函数调用出错。
其中最复杂的就是step3,这些可行函数也有优先级之分,优先级 从高到低是:
- 完全匹配
- 提升转化 (如,char short 转化成int,float 转化成 double)
- 标准转化 (如,int 转化成 char ,long转化成double)
- 用户定义的转化 (如类声明中定义的转换)
而完全匹配中也有细小的优先级之分。 总而言之,在step3 中如果优先级最高的可行函数是唯一的那么就调用他,否则会出现诸如ambiguous 的错误。
这一节的目的就是完全理解编译器如何让处理如下代码:
#include<iostream>
using namespace std;
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
int main(){
may('B');
}
void may(int a){
cout<<1<<endl;
}
float may(float a,float b){
cout<<2<<endl;
return a;
}
void may(char &a){
cout<<3<<endl;
}
char* may(const char* a){
cout<<4<<endl;
return NULL;
}
char may(const char &a){
cout<<5<<endl;
return a;
}
template<class T>
void may(const T & a){
cout<<6<<endl;
}
template<class T>
void may(T *){
cout<<7<<endl;
}
上述代码没有一点问题,甚至连warning都没有,你可以自己试一下结果是什么。
'B' 是const char类型的 #1~#7都是候选函数,因为函数名字相同。 其中#1、#2、#3、#5、#6是可行函数,因为const char 类型无法隐式转换成指针类型,所以#4、#7不行,而其他函数通过隐式转换后参数是正确的。 #1是提升转换,#2是标准转换,#3、#5、#6是完全匹配,完全匹配中非模板函数比模板函数优先级高,所以#3、#5优先级高于#6,而由于const参数优先和const引用参数匹配,所以#5的优先级更高。 则#5>#3>#6>#1>#2,所以调用#5。
6.2 完全匹配中的三六九等
完全匹配函数包括:
- 不需要进行隐式类型转化的函数(即参数正确的函数)显然是完全匹配函数。
- 需要进行隐式类型转换,但是这些转换是无关紧要转换。
完全匹配允许的无关紧要转换:
实 参 | 形 参 |
---|
Type | Type& | Typc& | Type | Type[] | * Type | Type (argument-list) | Type ( * ) (argument-list) | Type | const Type | Type | volatile Type | Type * | const Type | Type* | volatile Type * |
- 常规函数优先级高于模板。
- 对于形参是指针或引用类型的函数,const修饰的实参优先匹配const修饰的形参,非const修饰的实参优先匹配非const修饰的形参。
- 较具体的模板优先级高于较简略的模板。(例如,显式具体化函数优先级高于常规模板)
#include<iostream>
using namespace std;
struct apple{
string name;
double weight;
int group;
};
void may(const apple & a){
cout<<1<<endl;
}
void may(apple &a){
cout<<2<<endl;
}
int main(){
apple a={"Alice",250.00,1};
may(a);
}
结果是2
#include<iostream>
using namespace std;
struct apple{
string name;
double weight;
int group;
};
void may(const apple & a){
cout<<1<<endl;
}
void may(apple &a){
cout<<2<<endl;
}
void may(apple a){
cout<<3<<endl;
}
int main(){
apple a={"Alice",250.00,1};
may(a);
}
这个编译器会出错,因为这三个函数都是完全匹配,但是#2 和 #3的优先级无法区别,记得吗,完全匹配中的优先级法则的第2条法则,只适用于形参是引用或者指针。
#include<iostream>
using namespace std;
struct apple{
string name;
double weight;
int group;
};
template<typename T>
void may(T a){
cout<<1<<endl;
}
template<typename T>
void may(T *a){
cout<<2<<endl;
}
int main(){
apple a={"Alice",250.00,1};
may(&a);
}
终端输出是2,&a 的类型是 apple* ,而#2明确指出形参是个指针,所以#2更具体。
关于如何找出最具体的模板的规则被称为部分排序规则。
- 部分排序规则:在实例化过程中,函数优先和转换少的模板匹配。也可以这么说,实参和形参越相似,模板越优先。
举个栗子:
#include<iostream>
using namespace std;
template<typename T>
void may(T a[]){
cout<<1<<endl;
}
template<typename T>
void may(T *a[]){
cout<<2<<endl;
}
template<typename T>
void may(const T *a[]){
cout<<3<<endl;
}
int main(){
double a[5]={1,2,3,4,5};
const double* b[5]={&a[0],&a[1],&a[2],&a[3],&a[4]};
may(a);
may(b);
}
may(a) 会和#1匹配,因为a的类型是double数组,double数组无法转换成指针数组,所以#2,#3不是可行函数。而对于may(b) ,他会和#3匹配。b的类型是cont指针数组,首先#1和#2和#3都是可行函数,而且都是完全匹配函数,因为#1 会实例化成may<const double*>(b) ,#2 他实例化成may<const double>(b) ,#3会实例化为may<double>(b) 所以我们看看那个模板更具体?#3模板直接指出了 形参是一个const指针数组,所以他最具体,#3优先级最高;其次是#2因为它的形参指出了是指针数组;#1是最不具体的,#3>#2>#1.
6.3总结
可行函数中优先级从高到低排列 | | |
---|
完全匹配 | 常规函数 | 形参若是指针或引用,注意const和非const | | 模板 | 较具体的模板优先级更高 | 提升转换 | | | 标准转换 | | | 用户定义转换 | | |
Swap<>(a,b) 这种代码,类似于显式实例化,但是<>中没有指出typename,所以这段代码是要求优先选择模板函数。
对于多参数的函数,优先级会非常复杂,就不谈了。
7.模板的发展
#include<iostream>
using namespace std;
template<typename T1,typename T2>
auto Add(T1 a, T2 b){
decltype(a+b) c;
c=a+b;
return c;
}
int main(){
int a=2;
double b=2.123;
cout<<Add(a,b);
}
关键字decltype 和 auto ,在模板中无法确定数据类型时,发挥了巨大的作用。
|