详解C++中函数模板的定义与使用

目录
  • 1. 前言
  • 2. 初识函数模板
    • 2.1 语法
    • 2.2 实例化
    • 2.3 实参推导
  • 3. 重载函数模板

1. 前言

什么是函数模板?

理解什么是函数模板,须先搞清楚为什么需要函数模板。

如果现在有一个需求,要求编写一个求 2 个数字中最小数字的函数,这 2 个数字可以是 int类型,可以是 float 类型,可以是所有可以进行比较的数据类型……

常规编写方案:针对不同的数据类型编写不同的函数。

#include <iostream>
using namespace std;
//针对 int 类型
int getMin(int num1,int num2) {
   return num1>num2?num2:num1;
}
//针对  float 类型
float getMin(float num1,float num2) {
   return num1>num2?num2:num1;
}
//针对 double 类型
double getMin(double num1,double num2) {
   return num1>num2?num2:num1;
}  

int main() {
    //整型数据比较
    int min=getMin(10,4);
    cout<<min<<endl;
    //float 类型数据比较
    float minf=getMin(3.8f,2.9f);
	cout<<minf<<endl;
    //double 类型数据比较
	double mind=getMin(1.8,2.1);
	cout<<mind<<endl;
	return 0;
}

重载函数(当然上述几个函数名也可以不相同)可以解决这个问题。显然,上述 3 个函数的算法逻辑是一模一样的,仅是函数的参数类型不一样。既然函数的形式参数可以接受值不同的同类型数据,能否把函数形参的数据类型也参数化,用来接受不同的数据类型。

函数模板实质就是参数化数据类型,称这种编程模式为数据类型泛化编程。

Tips: 泛化的意思是一般化、抽象化,先不明确指定,需要时再指定。

如:我对班长说,我需要一名学生帮我搬课桌。这名学生到底是谁,我没有明确,由班长具体化。换在函数模板中,表示函数模板需要一种数据类型的数据,具体是什么数据类型,由使用者决定。

2. 初识函数模板

2.1 语法

在重构上述代码时,先了解一下函数模板的语法结构:

template <模板形式参数列表>  返回类型 函数名(函数形式参数列表)
{
函数体
}

语法结构说明:

1.template关键字说明了此函数是一个函数模板。

2.template <>的尖括号里是模板参数列表,也可称此处的参数为数据类型参数,用来对函数算法所针对的数据类型的泛化,表示可以接受不同的数据类型。

Tips:模板参数列表中的参数可以是一个或多个泛化数据类型参数,也可以是一个或多个具体数据类型参数。

泛化类型参数前面要加上 typename 关键字。

3.后面便是函数的一般性说明,只是在函数中可以使用模板数据类型参数。

Tips: 函数模板中有 2 类参数,模板参数和函数参数。

使用函数模板重构上面求最小值的代码:

template<typename T> T getMin(T num1,T num2){
	return num1>num2?num2:num1;
}

说明:

1.typename T声明了一个数据类型参数,用于泛化任一种数据类型,或者说 T可以表示任意一种数据类型。

Tips:typename 是 C++11 标准,也可以使用 class关键字,但建议不用,避免和类定义混淆。

2.T数据类型可以作为函数的参数类型、返回值类型、以及作为算法实施过程中临时变量的数据类型。

Tips: T是一个变量标识符,在遵循变量命名规则的前提下,可以起任意名称。

2.2 实例化

函数模板如现实生活中制作陶瓷的模具一样,只有往模具中注入原材料,才能生成可实用的陶瓷。函数模板不是函数,仅是一个模板,不能直接调用,需要实例化后才能调用。

实例化:指编译器根据开发者对函数模板注入的具体(实参)数据类型构造出一个真正的函数实体(实例),这个过程由编译器自动完成,且实例化的函数对于开发者不可见。

int res= getMin<int>(1,6);
cout<<res<<endl;
//输出结果:1

如上,编译器通过函数模板<>内的int数据类型,实例化的函数可以对 int类型的数据进行算法操作。同理,下面的代码会让编译器实例化针对不同数据类型的数据进行算法操作的函数。

//实例化原型为 float  getMin(float num1,float num2){函数体} 的函数
float resf=getMin<float>(3.2f,8.2f);
cout<<resf<<endl;
//实例化原型为 double  getMin(double num1,double num2){函数体} 的函数
double resd=getMin<double>(1.2,0.2);
cout<<resd<<endl;
//实例化原型为 char  getMin(char num1,char num2){函数体} 的函数
char resc=getMin<char>('A','B');
cout<<resc<<endl;
//输出结果分别为  3.2f  0.2  A

使用函数模板的优点不言而喻,声明一次,便可以实现针对不同数据类型的数据的操作。当然,中间会有匹配、实例化的代价。

Tips:高级业务层面的一劳永逸往往会以牺牲底层的性能为代价,但是,这是值得的。

除了通过显示声明数据类型提示编译器实例化,也可以使用函数指针实例化。

typedef int(*PF)(int,int); // 1
PF pf=getMin;  // 2
int res= pf(6,8);  //3
cout<<res;  //4

说明:

处先定义一个函数指针类型。

处这行代码,千万不要理解是取函数模板的地址,编译器在底层做了相应处理。

编译器会根据函数指针类型说明先实例化一个函数。

再取实例化函数的内存地址,并赋值给 pf

3 处以函数指针方式调用函数。

实例化时要注意的几个问题:

1.实例化时,可能会有一个直观问题:真的能指定任意一种数据类型实例化函数模板吗?

答案是:任何高级层面的逻辑行为都不能脱离基础知识的认知范畴,不同的数据类型有着语法系统赋予它的运算操作能力,当指定一个不支持函数模板内部算法操作的数据类型时,必然会出错。

如声明一个求 2 个数字相除的余数的函数模板。

template<typename T> T getYuShu(T num1,T num2) {
	return num1 % num2;
}

如果指定 double 数据类型实例化 getYuShu 函数模板时,就会抛出错误,因为 double数据类型不能使用 %运算符。

double res=getYuShu<double>(6.2,2.4);  //出错

Tips: 编译器在实例化函数模板时,会遵循语法标准检查给定的数据类型是否支持函数模板中的运算操作。

2.编译器实例化的时机。

常规而言,编译器会在程序中第一次需要函数模板的某个实例时对其进行编译。但是,同一份代码中,可能会出现对同一个实例多次调用的需要,如下面的代码:

template <typename T > test(T num) {
	return num;
}
int f() {
	int res= test<int>(12);
	return res;
}
double f1() {
	int res= test<int>(24);
	return double(res);
}

ff1函数都需要使用 test<int>实例,于编译器而,无法知道 ff1函数谁先会被调用(也就无法确定第一次编译的时间点),但为了保证编译期间完成实例化工作,早期C++编译器采用对同一实例每一次出现的地方都编译的策略,然后从多个编译结果中选一个作为最终结果,显然,编译时间会大大延长。

C++充许显式实例化声明,用来显示指定某一个函数模板的实例化的时间点,从而解决同一个实例被多次编译的问题。其语法如下:

template 返回值类型 模板名<模板参数列表>(函数形参列表);

针对上述函数模板可以编写如下代码,告之编译器编译时间点。

template <typename T > test(T num) {
	return num;
}
//显示指定实例化
template int test<int>(int);

Tips: 显示声明只对一个源文件有效。

2.3 实参推导

所谓实参推导,在使用函数模板时省略<>,不明确指定数据类型参数,而是由编译器根据函数的实参类型自动推导出类型参数的真正类型。如下代码:

int res=getMin(4,7);

实参是int 类型, 编译器由此推导出 T 是 int类型,从而使用 int类型实例化函数模板,类似于下面的显示声明代码:

int res=getMin<int>(4,7);

实参推导可以像调用普通函数一样使用函数模板。但是实参推导是有前提条件的:函数参数使用了类型参数的才能通过函数实参类型推导。如下的函数模板。

template <typename T1,typename T2> T2 myMax(T1 num1,T1 num2) {
	//函数体
}

因为 T2是作为函数模板的返回类型,是无法通过实参类型推导出来的。如下图所示:

使用如上函数模板,需要显示指定具体的数据类型。

double res= myMax<int,double>(6,8); //正确

是否可以让函数模板的类型参数一部分显示指定,一部分由实参推导?

答案是可以,但是,要求在声明函数模板时,把需要显示指定的类型参数放在前面,可由实参推导的参数类型放在后面。把上面的函数模板的 T1、T2参数说明交换位置。

template <typename T2,typename T1> T2 myMax(T1 num1,T1 num2) {
	//函数体
}

实例化时,只需要显示指定 T2的类型,T1类型由编译器根据实参推导。如下代码可正确调用。

double res= myMax<double>(6,8); //正确

编译器把 T2指定为 double类型,然后根据实参68推导出 T1是 int类型。

了解什么是实参推导后,使用时,需要知道实参推导是不支持自动类型转换的。如下代码是错误的。

int res=getMin(4,7.5); //错误

编译器认定实参 4int类型,实参7.5是 double类型,那么是到底是使用 int 类型还是使用 double类型实例化 getMin 函数模板,会让编译器不知所措、左右为难。

Tips: 即使支持自动类型转换,于编译器而言也无法知道开发者是想使用 int 类型还是 double 类型。如此自动类型转换没有存在的意义。

对于上述问题可以采用如下几种方案解决:

1.通过强制类型操作把实参转换成统一数据类型。

int res=getMin(4,int(7.5));
//或者
int res=getMin(double(4),7.5);

2.显示指定实例化时的数据类型。

int res=getMin<int>(4,7.5);
//或者
int res=getMin<double>(4,7.5);

3.如果有必要传递 2 个不同类型的参数,可需要修改函数模板,使其能接受 2 种类型参数。

template<typename T1,typename T2> T1 getMin(T1 num1,T2 num2){
	return num1>num2?num2:num1;
}

3. 重载函数模板

C++中普通函数和函数模板可以一起重载,面对多个重载函数,编译器需要提供相应的匹配策略。如下代码:

//普通函数
int getMax(int num1,int num2){
	return num1>num2?num1:num2;
}
//函数模板
template<typename T> T getMax(T num1,T num2) {
	return num1>num2?num1:num2;
}

如下调用时,编译器是选择普通函数还是函数模板?

int res= getMax(6,8);

函数实参是 int类型,相比较函数模板,普通函数不需要实例化可直接使用,编译器会优先选择普通函数。但是如下的调用,编译器会选择函数模板。

getMax(2.4,6.8); //调用 getMax<double>(实参推导)
getMax('a','b'); //调用 getMax<char>(实参推导)
getMax<>(7,3) //调用 getMax<int> (实参推导)
getMax<double>(4,9) //显示指定

编译器选择函数模板的原则:

如果函数模板能实例出一个完全与函数实参类型相匹配的函数,那么就会选择函数模板,如getMax(2.4,6.8); 调用。编译器会根据函数模板实例化一个double getMax(double a,double b)函数与需求完全相匹配的函数。

如果即想使用实参推导,且想使用函数模板而非普通函数,可以使用空 <>尖括号语法。如上的 getMax<>(7,7);调用。一旦指定<>标识符,显示指定使用函数模板,无论其中是否有实参类型说明。

如下的函数调用,实参有 2 个,但 2者之间可以发生自动类型转换。

charint之间可以相互转换。

getMax('a',98);

编译器会选择谁?可以做一个实验,把普通函数注释,保留函数模板。

#include <iostream>
#include <cstring>
using namespace std;
//函数模板
template<typename T> T getMax(T num1,T num2) {
	return num1>num2?num1:num2;
}
int main(int argc, char** argv) {
    int t= getMax('a',98)
	return 0;
}

执行后:

再恢复普通函数后执行,代码可以正常执行。显然,编译器选择的是普通函数。原因很简单,在使用实参推导时,函数模板是不支持自动类型转换,而普通函数表示没有压力。

总结一下,选择时,编译器会先考虑有没有类型完全相匹配的普通函数,没有,试着看能不能实例化一个完全匹配的函数。

以上就是详解C++中函数模板的定义与使用的详细内容,更多关于C++函数模板的资料请关注我们其它相关文章!

(0)

相关推荐

  • 简单掌握C++中的函数模板

    1.函数模板的声明和模板函数的生成 1.1函数模板的声明 函数模板可以用来创建一个通用的函数,以支持多种不同的形参,避免重载函数的函数体重复设计.它的最大特点是把函数使用的数据类型作为参数. 函数模板的声明形式为: template<typename 数据类型参数标识符> <返回类型><函数名>(参数表) { 函数体 } 其中,template是定义模板函数的关键字:template后面的尖括号不能省略:typename(或class)是声明数据类型参数标识符的关键字,

  • 详解C++函数模板与分离编译模式

    1.分离编译模式 一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件连接起来形成单一的可执行文件的过程称为分离编译模式. 2.使用函数模板在链接时出错 在C++程序设计中,在一个源文件中定义某个函数,然后在另一个源文件中使用该函数,这是一种非常普遍的做法.但是,如果定义和调用一个函数模板时也采用这种方式,会发生编译错误. 下面的程序由三个文件组成:func.h用来对函数模板进行申明,func.cpp用来定义函数模板,main.cpp包含func.h头文件

  • 深入解析C++中的函数模板和函数的默认参数

    C++函数模板 我们知道,数据或数值可以通过函数参数传递,在函数定义时它们是未知的,只有在发生函数调用时才能确定其值.这就是数据的参数化. 其实,数据类型也可以通过参数来传递,在函数定义是可以不指明具体的数据类型,当发生函数调用时,编译器可以根据传入的参数自动确定数据类型.这就是数据类型参数化. 所谓函数模板,实际上是建立一个通用函数,其返回值类型和形参类型不具体指定,用一个虚拟的类型来代替(实际上是用一个标识符来占位).这个通用函数就称为函数模板(Function Template).凡是函数

  • C++函数模板的使用详解

    函数模板可以适用泛型来定义函数,其中泛型可以是(int, double, float)等替换.在函数重载过程中,通过将类型作为参数传递给模板,可使编译器自动产生该类型的函数. 工作原理:比如需要定义一个比大小的max函数,有三种类型的数据(int,double,float),可能就需要编写三个函数,这样既浪费时间,且容易出错.如: #include <iostream> using namespace std; int Max(int a, int b); double Max(double

  • C++中函数模板的用法详细解析

    定义 我们知道函数的重载可以实现一个函数名多用,将功能相同或者类似函数用同一个名来定义.这样可以简化函数的调用形式,但是程序中,仍然需要分别定义每一个函数. C++提供的函数模板可以更加简化这个过程. 所谓函数模板实际上是建立一个通用函数,其涵涵素类型额形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板. 凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需要在模板中定义一次即可.在调用函数时,系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能

  • 详解C++中函数模板的定义与使用

    目录 1. 前言 2. 初识函数模板 2.1 语法 2.2 实例化 2.3 实参推导 3. 重载函数模板 1. 前言 什么是函数模板? 理解什么是函数模板,须先搞清楚为什么需要函数模板. 如果现在有一个需求,要求编写一个求 2 个数字中最小数字的函数,这 2 个数字可以是 int类型,可以是 float 类型,可以是所有可以进行比较的数据类型…… 常规编写方案:针对不同的数据类型编写不同的函数. #include <iostream> using namespace std; //针对 int

  • 详解Python中for循环的定义迭代方法

    目录 Python的 for 循环 遍历字典 range()功能 break语句 和continue语句 else语句 Python的 for 循环 Python 是基于集合的迭代. for <var> in <iterable>: # <iterable>是对象的集合--例如,列表或元组. <statement(s)> # 循环体 a = ['曹操', '孫権', '劉備'] for i in a: print(i) 输出: 曹操孫権劉備 可迭代对象 ,可

  • 详解laravel中blade模板带条件分页

    Blade模板简介 问: 什么是Blade模板? 答: Blade模板是Laravel提供一个既简单又强大的模板引擎: 和其他流行的PHP模板引擎不一样,他并不限制你在视图里使用原生PHP代码: 所有Blade视图页面都将被编译成原生的PHP代码并缓存起来,除非你的模板文件被修改,否则不会重新编译. 而这些都意味着Blade不会给我们增加任何负担. 在其他框架中,分页可能是件非常痛苦的事,Laravel 让这件事变得简单.易于上手.Laravel 的分页器与查询构建器和 Eloquent ORM

  • 详解Django中views数据查询使用locals()函数进行优化

    优化场景 利用视图函数(views)查询数据之后可以通过上下文context.字典.列表等方式将数据传递给HTML模板,由template引擎接收数据并完成解析.但是通过context传递数据可能就存在在不同的视图函数中使用重复的查询语句,所以可以通过将重复查询语句设置全局变量,配合locals()函数进行数据查询与传递. 优化前 def index(request): threatname = '威胁情报展示' url = 'www.testtip.com' allthreat = Threa

  • 详解c++中的trait与policy模板技术

    目录 概述 trait模板技术 用模板参数来传递多种trait policy模板技术 模板化的policy trait模板与policy模板技术的比较 概述 我们知道,类有属性(即数据)和操作两个方面.同样模板也有自己的属性(特别是模板参数类型的一些具体特征,即trait)和算法策略(policy,即模板内部的操作逻辑).模板是对有共性的各种类型进行参数化后的一种通用代码,但不同的具体类型又可能会有一些差异,比如不同的类型可能会有自己的不同特征和算法实现策略. trait模板技术 当在模板代码中

  • 详解python中的lambda与sorted函数

    lambda表达式 python中形如: lambda parameters: expression 称为lambda表达式,用于创建匿名函数,该表达式会产生一个函数对象. 该对象的行为类似于用以下方式定义的函数: def <lambda>(parameters): return expression python中的lambda函数可以接受任意数量的参数,但只能有一个表达式.也就是说,lambda表达式适用于表示内部仅包含1行表达式的函数.那么lambda表达式的优势就很明显了: 使用lam

  • 详解 MySQL中count函数的正确使用方法

    1. 描述 在MySQL中,当我们需要获取某张表中的总行数时,一般会选择使用下面的语句 select count(*) from table; 其实count函数中除了*还可以放其他参数,比如常数.主键id.字段,那么它们有什么区别?各自效率如何?我们应该使用哪种方式来获取表的行数呢? 当搞清楚count函数的运行原理后,相信上面几个问题的答案就会了然于胸. 2. 表结构 为了解决上述的问题,我创建了一张 user 表,它有两个字段:主键id和name,后者可以为null,建表语句如下. CRE

  • 详解MySQL中的存储过程和函数

    目录 区别 优点 创建储存函数和过程 储存过程 储存函数 查看储存过程 操作 变量 赋值 变量例子 定义条件和处理过程 条件 处理程序 游标 流程控制语句 储存过程和函数就是数据器将一些处理封装起来,调用 区别 调用储存过程只需要使用CALL,然后指定储存名称和参数,参数可以是IN.OUT和INOUT 调用储存函数只需要使用SELECT,指定名称和参数,储存函数必须有返回值,参数只能是IN 优点 良好的封装性 应用程序和SQL逻辑分离 让SQL也具有处理能力 减少网络交互 能够提高系统性能 降低

  • 详解MySQL中存储函数创建与触发器设置

    目录 1.创建存储函数 2.调用存储函数 3.创建触发器 4.在触发器中调用存储过程 5.删除触发器 存储函数也是过程式对象之一,与存储过程相似.他们都是由SQL和过程式语句组成的代码片段,并且可以从应用程序和SQL中调用.然而,他们也有一些区别: 1.存储函数没有输出参数,因为存储函数本身就是输出参数. 2.不能用CALL语句来调用存储函数. 3.存储函数必须包含一条RETURN语句,而这条特殊的SQL语句不允许包含于存储过程中 1.创建存储函数 使用CREATE FUNCTION语句创建存储

  • 详解Javascript 中的 class、构造函数、工厂函数

    到了ES6时代,我们创建对象的手段又增加了,在不同的场景下我们可以选择不同的方法来建立.现在就主要有三种方法来构建对象,class关键字,构造函数,工厂函数.他们都是创建对象的手段,但是却又有不同的地方,平时开发时,也需要针对这不同来选择. 首先我们来看一下,这三种方法是怎样的 // class 关键字,ES6新特性 class ClassCar { drive () { console.log('Vroom!'); } } const car1 = new ClassCar(); consol

随机推荐