,带你玩转 函数模板以及重载解析,函数还可以这样玩?

Posted 敲代码的Messi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了,带你玩转 函数模板以及重载解析,函数还可以这样玩?相关的知识,希望对你有一定的参考价值。

⚪⚪函数重载不是很了解的同学可以看看:点这里

前言

场景:编写一个函数,返回两个数相加的值。

  1. 假设是两个 int类型的数,我们需要声明原型:int add(int a,int b);
  2. 假设是两个 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);

这就是一个简单的函数模板,有三点需要注意:

  1. class 关键字可用 typename 代替。
  2. 模板并不创建函数,只是告诉告诉了编译器如何定义函数。只有当我们实际调用函数时,才会为特定类型生成函数定义
  3. 如果有多个类型的形参,在第一行我们这样写:
    class T1,class T2…
  4. 关于返回类型,后面会讲。

调用:

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

详细用法:

  1. 如果 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
  1. 如果expression是一个函数调用,则var的类型和该函数返回值的类型相同。
decltype (add(2,3)) var;

: 并没有实际调用函数,编译器通过查看函数原型获得返回类型。

  1. 如果 expression是一个用双括号括起来的左值,则var的类型为为左值类型的引用
int a=6;
int b=8;
decltype ((a)) c=b;
decltype 

此时 c 的类型为 int & ,并且是对 b的引用。直白的来讲:((a))=int &
引用想更加深入了解的,可以看看这篇文章,保你有收获。点这里

  1. 如果以上都不满足,则 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。因为它们他俩基本有一个相同的功能—自动获得变量类型

它俩该如何选择?区别是什么?

区别:

  1. auto根据值 来推演类型,所以我们用auto来声明定义变量时,必须要初 始化
  2. 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;

注:

  1. 注意具体化模板格式上和普通模板的微小差别
  2. 函数名后面的 < str > 可选
  3. 能存在具体化模板的前提是,已声明普通模板。

Ⅳ- 实例化和具体化

我们前面说过,函数模板本身不会生成函数定义。它只是给编译器提供了一个生成函数定义的方案
当我们调用模板生成函数定义时,得到的是模板实例。这种实例化方式被称为隐式实例化

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. 显式实例化会生成实例;显式具体化模板不会生成实例。


Ⅴ- 重载解析

现在,我们已经学习了 普通函数(函数重载),函数模板,显示具体化模板,模板重载。它们都有一个相同的作用为函数调用提供函数定义

1.问题引入

当有多个选择时,意思就是一个函数调用可能会有多个函数定义与其匹配,例如 非模板函数,模板函数等,模板函数。那此时应该选择哪一个呢?

我们需要在之间进行一个决策,决策的过程,我们称为—重载解析

2.大致过程

  1. 创建候选函数列表。其中包含与调用函数名相同函数模板
  2. 使用候选列表创建可行函数列表。这些都是函数参数正确的函数。
  3. 确定是否有最佳的可行函数,有则使用,没有报错

3.优先级

在有多个函数原型时,编译器首选看参数的匹配情况
该优先级最优到最差如下:

  1. 完全匹配。
  2. 提升转换。
  3. 标准转换。
  4. 用户定义的转换(类,结构)。

参数匹配优先级相同的前提下有:(非模板 >具体化模板> 模板)

值得注意的是:

我们在函数重载(点这里)中讲到的:当函数重载时,若有两个及以上的强转,会报错,但是不够完整。因为函数重载时,同样遵循参数优先级,即是若仅有一个强转是提升转换

提升与标准转换

什们是提升转换?什们是标准转换?

  1. 提升转换:
    相同类型的前提下,一些字节小的类型转换为字节大的类型。例:short -> int(char 同样),float -> double 等为提升转换。
  2. 标准转换:
    当然就是不是提升转换的转换,例: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;

运行结果:

注:

  1. 普通函数中,short -> double 是标准转换
  2. 具体化模板中,short -> int 是提升转换
  3. 普通模板中,泛型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);
  1. 当 T 被解释成 int * 时,#1 是完全匹配。
  2. 当 T 被解释成 int 时,# 2是完全匹配。

都是完全匹配,但是编译器会选择 #2 ,因为它进行的转换更少。也就是说,它是最佳匹配。


Ⅶ - 自定义选择

我们可以通过合适的函数调用语法,达到在普通函数函数模板自由选择

原型:

template <class T> void fun(T t);
void fun(int i);

调用:

int a=6;
fun<>(a);

在调用中,<>括号指出,我们选择函数模板
值得注意的是,语法不要和显式具体化模板和显式实例化混淆。

以上是关于,带你玩转 函数模板以及重载解析,函数还可以这样玩?的主要内容,如果未能解决你的问题,请参考以下文章

江哥带你玩转C语言 | 08 - C语言函数

18 | 眼前一亮:带你玩转GUI自动化的测试报告

一篇文章带你玩转Mac Finder

手把手教你玩转Canvas

30分钟带你玩转正则表达式

江哥带你玩转C语言 | 15- 修饰符和预处理指令