C++函数模板与重载解析
Shawn-Summer 人气:01.快速上手
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数。
#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 ,在模板中无法确定数据类型时,发挥了巨大的作用。
加载全部内容