C++函数模板与重载解析

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

C++函数模板与重载解析

Shawn-Summer   2022-09-29 我要评论

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,这些可行函数也有优先级之分,优先级 从高到低是:

  1. 完全匹配
  2. 提升转化 (如,char short 转化成int,float 转化成 double)
  3. 标准转化 (如,int 转化成 char ,long转化成double)
  4. 用户定义的转化 (如类声明中定义的转换)

而完全匹配中也有细小的优先级之分。

总而言之,在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 完全匹配中的三六九等

首先什么是完全匹配?

完全匹配函数包括:

  • 不需要进行隐式类型转化的函数(即参数正确的函数)显然是完全匹配函数。
  • 需要进行隐式类型转换,但是这些转换是无关紧要转换。

完全匹配允许的无关紧要转换:

实 参形 参
TypeType&
Typc&Type
Type[]* Type
Type (argument-list)Type ( * ) (argument-list)
Typeconst Type
Typevolatile 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 ,在模板中无法确定数据类型时,发挥了巨大的作用。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们