IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 从C到C++___函数模板 -> 正文阅读

[C++知识库]从C到C++___函数模板

函数模板

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

  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 完全匹配中的三六九等

  • 首先什么是完全匹配?

完全匹配函数包括:

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

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

实 参形 参
TypeType&
Typc&Type
Type[]* Type
Type (argument-list)Type ( * ) (argument-list)
Typeconst Type
Typevolatile Type
Type *const Type
Type*volatile Type *
  • 完全匹配中的优先级法则
  1. 常规函数优先级高于模板。
  2. 对于形参是指针或引用类型的函数,const修饰的实参优先匹配const修饰的形参,非const修饰的实参优先匹配非const修饰的形参。
  3. 较具体的模板优先级高于较简略的模板。(例如,显式具体化函数优先级高于常规模板)
#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);
}

关键字decltype 和 auto ,在模板中无法确定数据类型时,发挥了巨大的作用。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-07-17 16:02:42  更:2022-07-17 16:03:26 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 17:13:33-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码