C++函数模板与重载解析超详细讲解
作者:Shawn-Summer
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.重载的模板
如果对函数的重载不了解,可以翻看我之前的文章:
模板函数也可以重载,语法和常规函数的重载差不多;被重载的模板函数必须要特征标不同。
#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);
隐式实例化是指等你调用了这个函数的时候,它才会生成函数定义。
显式实例化:
template void Swap<int>(int,int);
显式实例化是指不需要等你调用这个函数,使用上面那段代码,直接能生成Swap<int>
函数的定义。 一般来说,我们会把模板放到一个头文件中,然后很多源文件会include它,然后编译的时候就会在这些源文件中生成具体化的代码。但是如果我们采用显式实例化,在其中一个源文件里面实例化一份代码,然后其他cpp文件用到的时候,通过链接程序找到这个代码并调用它,程序的大小就会少一些。这就是显式实例化的好处。
下面这段代码展示了Add<double>(a,b)
相较于Add(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的引用形参的时候,会产生临时变量,从而完成函数调用。总之,最好使用<type>
而不是根据参数类型自动生成模板的实例化.
显式隐式实例化和显式具体化统称为具体化或者实例化
上一节中我们提到了显式具体化,我们可以发现实例化和显式具体化的相同之处在于,他们都是使用具体类型的函数定义,而不是通用描述。
显式具体化函数是否是模板? 我的回答是:显式具体化函数是一个特殊的模板,它是专门为一种类型设计的模板。
//函数模板6.cpp #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);//显式具体化原型 template void Swap<char>(char&,char&);//显式实例化 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); char e='a'; char f='b'; Swap<char>(e,f);//调用显式实例化函数 cout<<"e:"<<e<<endl<<"f:"<<f<<endl; } 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; } template<class T> void Swap(T &a,T &b){ T temp; temp=a; a=b; b=temp; }
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类型会出现问题吗?
会报错,因为实参int
和函数中引用形参char&
的类型不一样,且此时不是const引用形参,也不会有临时变量产生。如果你不清楚,且看引用变量的语法。 内联函数、引用变量、函数重载
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.模板的发展
关键字decltype 和 auto
#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);}#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 ,在模板中无法确定数据类型时,发挥了巨大的作用。
到此这篇关于C++函数模板与重载解析超详细讲解的文章就介绍到这了,更多相关C++函数模板与重载解析内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!