C++:一篇文章,带你玩转 函数模板以及重载解析,函数还可以这样玩?
Posted 敲代码的Messi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++:一篇文章,带你玩转 函数模板以及重载解析,函数还可以这样玩?相关的知识,希望对你有一定的参考价值。
⚪⚪函数重载不是很了解的同学可以看看:点这里
文章目录
前言
场景:编写一个函数,返回两个数相加的值。
- 假设是两个 int类型的数,我们需要声明原型:int add(int a,int b);
- 假设是两个 double类型的数,我们需要声明原型:
double add(double a,double b);
分析:虽然用到了函数重载,但是用的是同样的技术,这样的代码就很冗杂,。不仅浪费我们的时间,并且容易出错。所以大佬们就为C++引进了一种新特性-----函数模板。
下面,我们慢慢感受它的魅力。
Ⅰ- 初识
在函数模板中,我们常用泛型来定义函数,函数在调用时,泛型被具体的类型代替。
函数有声明原型和定义,函数模板同样有。说着有点抽象,我们上实例:
程序 1.1:
因为模板很简单,所以这里将声明和定义写在一起:
template <class T>/这一行指出这是一个函数模板
int add(T t1,T t2){
return t1+t2;
}
int main(){
int a=2,b=3;
int c=add(a,b);
}
这就是一个简单的函数模板,有三点需要注意:
- class 关键字可用 typename 代替。
- 模板并不创建函数,只是告诉告诉了编译器如何定义函数。只有当我们实际调用函数时,才会为特定类型生成函数定义。
- 如果有多个类型的形参,在第一行我们这样写:
class T1,class T2… - 关于返回类型,后面会讲。
调用:
int count,a=2,b=3;
count=add(a,b);
此时, 泛型T 被 int 代替,生成对应的函数。
值得注意的是:函数模板不能减小程序的大小,有多少类型不同的调用,编译器就会生成相应的函数定义。
Ⅱ - decltype关键字
相信很多同学已经发现上面的问题,就是我们的返回类型是固定不变的,为了大家更好理解,将实例改一下:
程序 2.1:
template <class T1,class T2>
? add(T1 t1,T2 t2){
? count=t1+t2;
return count;
}
我们如何获得 count 的类型呢?
时代在进步,所以大佬们又把C++这个缺陷完善了,C++11新增的关键字----decltype(declare type) 提供了解决的办法。
1.瞅瞅decltype的两把刷子
通用格式:
decltype (expression) var
详细用法:
- 如果 expression 是一个标识符 ,则var的类型和该标识符的类型相同,包括了 cosnt 等限定符。
int i=6;
int &j=i;
const int ci=6;
decltype (j) var; //var is int &
decltype (ci) var; //var is const int
- 如果expression是一个函数调用,则var的类型和该函数返回值的类型相同。
decltype (add(2,3)) var;
注 : 并没有实际调用函数,编译器通过查看函数原型获得返回类型。
- 如果 expression是一个用双括号括起来的左值,则var的类型为为左值类型的引用。
int a=6;
int b=8;
decltype ((a)) c=b;
decltype
此时 c 的类型为 int & ,并且是对 b的引用。直白的来讲:((a))=int &
对引用想更加深入了解的,可以看看这篇文章,保你有收获。点这里。
- 如果以上都不满足,则 var 的类型与 expression 的实际类型相同
int x=6;
decltype (5) a;//a is int
decltype (x+6) b;//b is int
decltype (20L) c;// c is long
值得注意的是: 若为表达式,并不会实际去计算表达式的结果。
2.番外篇:auto和decltype的区别
decltype讲完了,不知道大家有没 有联想到 C++ 的另一大利器----auto。因为它们他俩基本有一个相同的功能—自动获得变量类型。
它俩该如何选择?区别是什么?
区别:
- auto是 根据值 来推演类型,所以我们用auto来声明定义变量时,必须要初 始化。
- decltype 是 根据表达式 ,标识符,函数 来推演类型。
auto a=6; //a is int
decltype (a) b;//b is int
3.后置返回类型
有了decltype,咱把之前函数模板改进一下:
程序 2.2:
template <class T1,class T2>
decltype (t1+t2) add(T1 t1,T2 t2){
decltype (t1+t2) count=t1+t2;
return count;
}
分析:
我们对 count 的声明是没有问题的。但是声明返回类型是有问题的,相信独具慧眼的你 已经看出来了,此时的 t1 和 t2 还没有定义。
有问题我们就要解决问题(当然是大佬们解决的),大佬们新增了一种声明和定义函数的语法:通过后置返回类型。
template <class T1,class T2>
auto add(T1 t1,T2 t2) -> decltype (t1+t2)
{
decltype (t1+t2) count=t1+t2;
return count;
}
此时,一个功能齐全的函数模板就大功告成了(麻雀虽小,五脏俱全)。
Ⅱ- 模板的重载
和常规重载一样,被重载的模板要求:函数特征标必须不同。
1.为什们需要模板重载?
因为在一个模板内,有一些语法对基本类型适用,但是可能对某些复合类型不适用,但是又要达到相同的目的:
a=b;
//这如果是 int 等基本类型,是正确的
//但是假设 a,b都是数组的话,是不可行的
模板的重载和普通函数的重载大同小异,大家可以看看这篇文章点这里。。。
Ⅲ - 显示具体化模板
显式具体化模板 是由普通推特殊的 结果,也可以说是 模板重载的延申。
在模板的实际运用中,我们会对某一特殊类型使用不同的语法,但是又用到相同的技术或者达到相同的目的。
场景: 交换两个变量的值
普通模板:
decltype <class T>
void swap(T &t1,T &t2){
T a=t1;
t1=t2;
t2=a;
}
场景的特殊化: 有如下的结构体,交换两个结构体中成员 a的值。
显式具体化模板:
struct str{
int a;
char c;
};
template <>
void swap<str>(str &s1,str &s2){
int temp=s1.a;
s1.a=s2.a;
s2.a=temp;
}
注:
- 注意具体化模板格式上和普通模板的微小差别。
- 函数名后面的 < str > 可选。
- 能存在具体化模板的前提是,已声明普通模板。
Ⅳ- 实例化和具体化
我们前面说过,函数模板本身并不会生成函数定义。它只是给编译器提供了一个生成函数定义的方案。
当我们调用模板生成函数定义时,得到的是模板实例。这种实例化方式被称为隐式实例化。
1.隐式实例化的缺点:
通过模板生成函数定义时,形参的类型只能由实参决定。
2.显式实例化:
C++如今允许显式实例化,意思就是通过命令可以创建特定的实例。
3.场景:
比如说通过模板需要一个函数,传入一个double类型的实参,但是我们在函数中只需要用到他的整数部分。(double 强转-> int)这就可以用显式实例化。
程序 4.1: 我们常用 函数调用 的方式来创建显显式实例化
template <class T>
void fun(T t1){
...
...
}
int main(){
double a=6.66;
fun<int>(a); //用函数的方式创建显示实例化
...
}
4.值得注意的是:
fun< int > (a,b) 中,< >中的类型与实参依次对应,假如说模板有两个形参,则<int,char>fun(6.66,25) 将 6.66转换为int 类型,25转换为char类型。
程序 4.2:使用 声明 的方式创建 显式实例化
template void fun<int>(int a);
5.最后总结一下:
- 注意 显示实例化,显式具体模板,普通模板在语法上的微小差别 。
- 显式实例化的作用:将实例的形参转换为我们需要的类型,然后看实参能否强制转换。
- 显式实例化,会生成实例;显式具体化模板,不会生成实例。
Ⅴ- 重载解析
现在,我们已经学习了 普通函数(函数重载),函数模板,显示具体化模板,模板重载。它们都有一个相同的作用:为函数调用提供函数定义。
1.问题引入
当有多个选择时,意思就是一个函数调用可能会有多个函数定义与其匹配,例如 非模板函数,模板函数等,模板函数。那此时应该选择哪一个呢?
我们需要在之间进行一个决策,决策的过程,我们称为—重载解析。
2.大致过程
- 创建候选函数列表。其中包含与调用函数名相同的函数和模板。
- 使用候选列表创建可行函数列表。这些都是函数参数正确的函数。
- 确定是否有最佳的可行函数,有则使用,没有报错。
3.优先级
在有多个函数原型时,编译器首选看参数的匹配情况。
该优先级最优到最差如下:
- 完全匹配。
- 提升转换。
- 标准转换。
- 用户定义的转换(类,结构)。
在参数匹配优先级相同的前提下有:(非模板 >具体化模板> 模板)。
值得注意的是:
我们在函数重载(点这里)中讲到的:当函数重载时,若有两个及以上的强转,会报错,但是不够完整。因为函数重载时,同样遵循参数优先级,即是若仅有一个强转是提升转换
提升与标准转换
什们是提升转换?什们是标准转换?
- 提升转换:
在相同类型的前提下,一些字节小的类型转换为字节大的类型。例:short -> int(char 同样),float -> double 等为提升转换。 - 标准转换:
当然就是不是提升转换的转换,例:int ->char,long->floatd等。
演练
1.参数优先级相同下:非模板 > 具体化模板 > 模板
template<class T> void fun(T t1){
cout<<"模板\\n";
}
template<> void fun(int i){
cout<<"具体化模板\\n";
}
void fun(int i){
cout<<"普通函数\\n";
}
int main() {
int a=6;
fun(a);
return 0;
}
运行结果:
2.参数优先级
template<class T> void fun(T t1){
cout<<"模板\\n";
}
template<> void fun(int i){
cout<<"具体化模板\\n";
}
void fun(double i){
cout<<"普通函数\\n";
}
int main() {
short a=6;
fun(a);
return 0;,
}
运行结果:
注:
- 普通函数中,short -> double 是标准转换。
- 在具体化模板中,short -> int 是提升转换。
- 普通模板中,泛型T -> int 是完全匹配。
所以运行结果为模板,值得注意的是:在普通函数中,几乎都是完全匹配。
Ⅵ - 最佳匹配
1. 问题引入
当一种类型的函数(非模板,模板)的两个重载版本都是完全匹配时,肯定会报错吗?
答案是不一定的,当发生此类场景的时候,编译器会推断使用哪种类型时执行的转换做少。
2.演练场
看下面这个模板原型:
// #1
template <class T> void fun(T t);
// #2
template <class T> void fun(T *t);
调用:
int a=6;
fun(&a);
- 当 T 被解释成 int * 时,#1 是完全匹配。
- 当 T 被解释成 int 时,# 2是完全匹配。
都是完全匹配,但是编译器会选择 #2 ,因为它进行的转换更少。也就是说,它是最佳匹配。
Ⅶ - 自定义选择
我们可以通过合适的函数调用语法,达到在普通函数和函数模板的自由选择。
原型:
template <class T> void fun(T t);
void fun(int i);
调用:
int a=6;
fun<>(a);
在调用中,<>括号指出,我们选择函数模板。
值得注意的是,语法不要和显式具体化模板和显式实例化混淆。
以上是关于C++:一篇文章,带你玩转 函数模板以及重载解析,函数还可以这样玩?的主要内容,如果未能解决你的问题,请参考以下文章