【C++】函数模板(看这一篇就够了)

函数模板

函数模板存在的意义

在现实生活中,为了提高工作效率,我们通常会用一些模板来完成一些事情,比如:年终总结的ppt,我们可以拿一些现成的ppt模板来写,通常我们只需将内容进行填充就可以了,而不需要从0到1的去制作ppt的各个方面,我们只需要关注最核心的地方(对我们而言,年终总结的内容才是最关键的)就可以了。

而在C++中,模板也是同样的道理,它的出现,大大提高了代码的复用性,使我们的代码更具通用性,更灵活!C++最重要的特性之一就是,代码的重用,为了实现代码重用,代码就必须具有通用性。通用代码不受数据类型的影响,并且可以自动适应数据类型的变化,这种程序设计类型称为参数化程序设计。

模板是C++支持参数化程序设计的工具,通过它可以实现参数化多态性。所谓参数化多态性是指:将程序所处理的对象的类型参数化,使得一段程序可以处理多种不同类型的对象。简单的理解,模板就是为了让代码的通用性更强。

有了以上的理解, 下面理解函数模板和类模板就轻松多了。

有了函数重载为什么还要函数模板?

要讲函数模板,就得先讲一讲函数重载,相信大家都知道,函数重载通常是对于不同的数据类型完成类似的操作。

在很多时候,一个算法其实是可以处理多种数据类型的。但是用函数实现算法时,即使设计为重载函数也只是

使用相同的函数名,函数体仍然要分别定义。下面举一个求绝对值的函数重载的实例,方便大家理解。

int abs(int x){
    return x<0?-x:x;
}
double abs(double x){
    return x<0?-x:x;
}

可以看出,这两个函数的功能和函数体是一模一样的,但是为了实现对不同数据类型的数据求绝对值操作,我们不得不写两个同名函数来实现操作。

如果能写一段通用代码适用与多种不同数据类型该多好呢,这会使代码的可重用性大大提高,从而提高软件的开发效率。所以便设计出了这么一个工具,那就是函数模板,程序员只需对函数模板编写一次,然后基于调用函数时提供的参数类型,C++编译器将自动产生相应的函数来正确处理该类型的数据。

函数模板的定义

函数模板的定义形式如下:

template<typename 类型名>
类型名  函数名(参数列表)
{
函数体的定义;
}

上述typename标识符是后来新出的标识符,更容易理解,之前的C++版本也可以使用class标识符替换这里的typename,该标识符指明可以接收一个类型参数,这些类型参数代表的是类型,可以是系统预定义的数据类型,也可以是自定义类型。类型参数可以用来指定函数模板本身的形参类型、返回值类型,以及声明函数中的局部变量。

例如,上面的函数可以用函数模板替换成:

template<typename T>
T abs(T x){
    return x<0?-x:x;
}

如何正确理解函数模板

函数模板并不是一个真正的函数,它只是一段编译指令,在你的程序下文调用某个类型的函数时,它才会真正的展开成一个这种类型的函数。

听起来可能有点抽象,不过这一点,我们可以通过观察汇编代码去较为清晰的证明。(这在里推荐一个比较好用的实时看汇编代码的网站:Compiler Explorer (godbolt.org)

首先,我们定义一个普通函数:

int abs(int x){
    return x<0?-x:x;
}

使用刚刚的网站,看一看它的汇编代码:

请添加图片描述

然后,我们定义一个函数模板:

template<typename T>
T abs(T x){
    return x<0?-x:x;
}

再次使用刚刚的网站,看一看它的汇编代码:

请添加图片描述

你会发现其对应的汇编代码是空的!而当我们在下面调用某种类型的函数时:

请添加图片描述

看见了吗?这里它生成了两种类型的abs函数,这是因为我们在主函数里调用了这两种类型的函数,也就是说,只有当我们实际调用的时候,这种类型的函数才会通过函数模板这一“蓝图”生成。只写一个模板函数而不去调用就什么函数也不会生成!

函数模板的使用

在上面我们这样调用了abs函数:

abs<int>(1);
abs<double>(1.0);

这种是通过显式调用,还有一种更方便的隐式调用:

abs(1);
abs(1.0);

在这种情况下,编译器会根据形参类型自动推导需要生成什么类型的abs函数。

模板就像是你在C++里雇佣的一个员工,你命令它,它帮你写代码。当然前提是你的命令得正确。

abs这个函数比较简单,下面我们再来看一个函数:

template<typename T>
T add(T x,T y){
    return x+y;
}

如果我们这样使用它:

cout<<add(1,1)<<endl;
cout<<add(1,2.9)<<endl;//这里由于两个参数类型不一致会报错

如何解决这种办法?

首先,我们可以显示的调用add函数:

cout<<add(1,1)<<endl;
cout<<add<int>(1,2.9)<<endl;

这时,形参会被强制转换成int类型,从而正常调用add函数。

还有一种更好的办法:

template<typename T,typename U>
T add(T x,U y){
    return x+y;
}

我们可以定义两种参数,但这个时候问题又来了:我们可以区分x和y的类型,但是x+y的类型呢?也就是说返回值类型如何确定?这里介绍一个关键字:decltype.

什么是decltype

​ decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。

auto varName=value;
decltype(exp) varName=value;
  • auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系;

  • auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导,而decltype不要求,因此可以写成如下形式

    decltype(exp) varName;
    

    原则上将,exp只是一个普通的表达式,它可以是任意复杂的形式,但必须保证exp的结果是有类型的,不能是void;如exp为一个返回值为void的函数时,exp的结果也是void类型,此时会导致编译错误

    int x = 0;
    decltype(x) y = 1;           // y -> int
    decltype(x + y) z = 0;       // z -> int
    const int& i = x;
    decltype(i) j = y;           // j -> const int &
    const decltype(z) * p = &z;  // *p  -> const int, p  -> const int *
    decltype(z) * pi = &z;       // *pi -> int      , pi -> int *
    decltype(pi)* pp = &pi;      // *pp -> int *    , pp -> int * *
    

decltype推导规则

  • 如果exp是一个不被括号()包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,decltype(exp)的类型和exp一致;
  • 如果exp是函数调用,则decltype(exp)的类型就和函数返回值的类型一致;
  • 如果exp是一个左值,或被括号()包围,decltype(exp)的类型就是exp的引用,假设exp的类型为T,则decltype(exp)的类型为T&;
  • 类的静态成员可以使用auto, 对于类的非静态成员无法使用auto,如果想推导类的非静态成员的类型,只能使用decltype。

左值:表达式执行结束后依然存在的数据,即持久性数据;右值是指那些在表达式执行结束不再存在的数据,即临时性数据。

一个区分的简单方法是:对表达式取地址,如果编译器不报错就是左值,否则为右值

template<typename T>
class A
{
private :
   decltype(T.begin()) m_it;
   //typename T::iterator m_it;   //这种用法会出错
public:
void func(T& container)
{
   m_it=container.begin();
}
};

int main()
{
const vector<int> v;
A<const vector<int>> obj;
obj.func(v);
return 0;
}

现在有了这个关键字,你可能会说这时候函数模板可以写成这样:

template<typename T,typename U>
decltype(x+y) add(T x,U y){
    return x+y;
}

但是你有没有想过,当编译器执行到decltype的时候,还并不知道(x+y)是个什么类型,自然也就无法进行推导。

所以应该怎么写呢?这里给出一种方案:利用构造函数:

template<typename T,typename U>
decltype(T()+U()) add(T x,U y){
    return x+y;
}

利用这种当时就可以实现不同类型的两个数相加。

这时候你可能会说,那如果T和U没有默认构造函数怎么办?像这种情况:

#include <iostream>

using namespace std;

class A{
	int x;
	public:
		A() = delete;
		A(int x):x(x){}
		
		A operator+(const A& b){
			return (x+b.x);
		}
		
		friend ostream& operator<<(ostream & os , const A& a){
			return os<<a.x;
		}
};

template<typename T,typename U>
decltype(T()+U()) add(T x,U y){
    return x+y;
}

int main(){
    A aa(3),bb(4);
    cout<<add(aa,bb)<<endl;
    return 0;
}

这种情况又该怎么办呢?

关于函数的返回值,其实C++提供了两种方式,我们常用的只是其中一种:

int getSum(int a, int b);

还有一种方式叫返回类型后置

auto getSum(int a, int b)->int;

那时候后面这种返回类型后置的用法就用上了:

#include <iostream>

using namespace std;

class A{
	int x;
	public:
		A() = delete;
		A(int x):x(x){}
		
		A operator+(const A& b){
			return (x+b.x);
		}
		
		friend ostream& operator<<(ostream & os , const A& a){
			return os<<a.x;
		}
};

template<typename T,typename U>
auto add(T x,U y)->decltype(x+y)
{
    return x+y;
}

int main(){
    A aa(3),bb(4);
    cout<<add(aa,bb)<<endl;
    return 0;
}

函数模板具体化

前面我们讨论过函数模板存在的意义,很显然,我们可以通过合理的使用函数模板实现泛型编程,也就是说定义一个函数模板,然后理想情况下支持任何形式的调用。但是事实上,即便我们定义的函数模板在大多数情况下能够满足我们的需求,然而总有一些特殊情况需要我们特殊处理,于是便有了隐式实例化,显式实例化,部分具体化,显式具体化。这些统称为函数具体化。

隐式实例化

我们前面提到的两种实例化的方式都是属于隐式实例化,这种实例化的特点是只有我们真正需要一个函数时,函数模板才会为我们实例化这种类型的函数。

显式实例化

相较于前面的隐式实例化,如果当使用者确认某种类型的函数在接下来一定会被使用的时候,我们可以为其显式实例化,相关语法为:

template void Swap<int>(int,int);//显式实例化

编译器看到上述声明后,将使用Swap( )模板生成一个使用int类型的实例。也就是说,该声明的意思是“使用Swap( )模板生成int类型的函数定义。”。这种实例化的好处是节约了在编译器生成这种函数的时间。

显式具体化

对于刚刚的函数模板:

template<typename T>
T abs(T x){
    return x<0?-x:x;
}

有时候可能会要求针对与某一种类型进行特殊处理,例如不仅仅是返回,还需要进行打印输出,这时候就需要针对这种情形进行“特化”:

template <> 
T abs<int>(int x){//这里可以省略<int>
    cout<<a<<endl;
    return x<0?-x:x;
} 

调用规则

  • 如果函数模板和普通函数都可以实现,优先调用普通函数;调用顺序:普通函数>显示具体化>模板函数
void myPrint(int a, int b)
{
	cout << "调用的普通函数" << endl;
}
 
template<typename T>
void myPrint(T a, T b)
{
	cout << "调用的是函数模板" << endl;
}
void Mytest()
{
	int a = 10;
	int b = 20;
	myPrint(a, b);
 
}	   
int main()
{
	Mytest();
	return 0;
}

//输出:调用的普通函数
  • 可以使用空模板参数列表来强制调用函数模板;
void myPrint(int a, int b)
{
	cout << "调用的普通函数" << endl;
}
 
template<typename T>
void myPrint(T a, T b)
{
	cout << "调用的是函数模板" << endl;
}
void Mytest()
{
	int a = 10;
	int b = 20;
	myPrint<>(a, b);    //通过空模板参数列表来强制调用函数模板
}   
int main()
{
	Mytest();
	return 0;
}

//输出:调用的是函数模板

函数模板也可以发生重载;

template<typename T>
void myPrint(T a, T b)
{
	cout << "调用的是函数模板" << endl;
}
 
template<typename T>
void myPrint(T a, T b,T c)
{
	cout << "调用的是重载的函数模板" << endl;
}
void Mytest()
{
	int a = 10;
	int b = 20;
	myPrint(a, b, 100);
 
}  
int main()
{
	Mytest();
	return 0;
}

//输出:调用的是重载的函数模板

如果函数模板可以产生更好的匹配,优先调用函数模板;

void myPrint(int a, int b)
{
	cout << "调用的普通函数" << endl;
}
 
template<typename T>
void myPrint(T a, T b)
{
	cout << "调用的是函数模板" << endl;
}
void Mytest()
{
	char c1 = 'a';
	char c2 = 'b';
	myPrint(c1, c2);
}   
int main()
{
	Mytest();
	return 0;
}

//调用的是函数模板

模板的重载和嵌套使用;

#include <iostream>
 
using namespace std;
template <class T>
T Max(T a,T b)
{
    return((a > b)?a : b);
}
template <class T>
T Max(T a,T b,T c)
{
    return Max(Max(a,b),c);    //嵌套
}
template <class T>
T Max(T a,T b,T c,T d)
{
    return Max(Max(a,b,c),d);    //嵌套
}
 
int main(int argc, char *argv[])
{
    cout << Max(8,11) << endl;
    cout << Max(4,8,9) << endl;
    cout << Max(4.2,2.8,5.9) << endl;
 
    cout << Max<float>(5.4,3.8,2.9) << endl;
    cout << Max<float>(5.4,3.8,2.9,8.1) << endl;
    return 0;
}

//输出结果
11
9
5.9
5.4
8.1

--------------------------------
Process exited after 0.08955 seconds with return value 0
请按任意键继续. .

总结

模板是一个通用框架,是C++泛型编程思想的主要体现。普通函数与函数模板的区别主要如下:

  • 普通函数只可以有一种数据类型相匹配。函数模板有多种类型
  • 隐式推导优先使用普通函数,只有普通函数不匹配才使用函数模板
  • 函数模板只有在调用时,才会构建函数,而普通函数是在编译时
  • 普通函数调用时候可以发生自动类型转换,而函数模板不行
int myAdd1(int a, int b)    //普通函数
{
	return a + b;
}
 
template<class T>
int myAdd2(T a, T b)    //函数模板
{
	return a + b;
}
 
void Mytest()
{
	int a = 10;
	int b = 20;
	char c = 'c';
	cout << myAdd1(a, c) << endl;
 
	//隐式类型推导
	cout << myAdd2(a, b) << endl;
	//cout << myAdd2(a, c) << endl;会报错,T的类型不一致
 
	//显示指定类型
	myAdd2<int>(a, c); //不会报错,会发生隐式类型转换
}
	   
int main()
{
	Mytest();
	return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_43717839/article/details/131519142