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 <模板形式参数列表>  返回类型 函数名(函数形式参数列表)
{
函数体
}

语法结构说明:

  • template关键字说明了此函数是一个函数模板

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

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

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

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

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

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

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

说明:

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

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

  • 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

说明:

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

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

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

    再取实例化函数的内存地址,并赋值给 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: 编译器在实例化函数模板时,会遵循语法标准检查给定的数据类型是否支持函数模板中的运算操作。

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

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

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 类型, 编译器由此推导出 Tint类型,从而使用 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推导出 T1int类型。

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

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

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

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

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

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

    int res=getMin(4,int(7.5));
    或者
    int res=getMin(double(4),7.5);
  • 显示指定实例化时的数据类型。

    int res=getMin<int>(4,7.5);
    //或者
    int res=getMin<double>(4,7.5);
  • 如果有必要传递 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;
}

执行后:

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

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

4. 总结

本文只讲到了函数模板的语法、实例化和重载 3 个方面的内容,除此之外,函数模板还有其它高级应用,受限于篇幅,本文仅抛砖引玉,有兴趣者可以查阅相关文档。

C++ 初识函数模板的更多相关文章

  1. day7学python 初识简单模板

    初识简单模板 模块与包 1.模块:用来从逻辑上组织python代码(变量,函数,类,逻辑:实现功能),本质是.py结尾的文件 但导入的模块名,无.py 2.包:从逻辑上组织模块,本质就是目录(含有_i ...

  2. C++进阶-1-模板基础(函数模板、类模板)

    C++进阶 模板 1.1 函数模板 1 #include<iostream> 2 using namespace std; 3 4 // 模板 5 6 // 模板的简单实例 7 // 要求 ...

  3. c++函数模板作为类的成员函数,编译报错LNK2019的解决方法

    为了使某个类的成员函数能对不同的参数进行相同的处理,需要用到函数模板,即template<typename T> void Function(). 编译时报错LNK2019 解决方法: 1 ...

  4. C++STL - 函数模板

    模板主要是为了泛型编程,做到与类型无关 模板有函数模板和类模板,本文主要整理的是函数模板 1.函数模板定义 template<typename 类型形参1,typename 类型形参2,...& ...

  5. 使用getopt_long来解析参数的小函数模板

    getopt_long原型 #define no_argument 0 #define required_argument 1 #define optional_argument 2 struct o ...

  6. C++函数重载和函数模板

    1.函数重载 这是小菜鸟写的一个例子. 函数重载应该注意以下几点: 1.1重载函数有类似的功能: 1.2只能以参数的类型(形参个数和类型)来重载函数, int max(int a,int b);flo ...

  7. 零值初始化&字符串常数作为函数模板参数

    1.在定义一个局部变量时,并希望该局部变量的初始化一个值,可以显示调用其默认构造函数,使其值为0(bool类型默认值为false). template <typename T> void ...

  8. 让gcc支持成员函数模板的trick

    让gcc支持成员函数模板的trick 罗朝辉 (http://www.cnblogs.com/kesalin/) 本文遵循“署名-非商业用途-保持一致”创作公用协议   gcc 4.7.3 不支持成员 ...

  9. 不可或缺 Windows Native (16) - C++: 函数重载, 缺省参数, 内联函数, 函数模板

    [源码下载] 不可或缺 Windows Native (16) - C++: 函数重载, 缺省参数, 内联函数, 函数模板 作者:webabcd 介绍不可或缺 Windows Native 之 C++ ...

随机推荐

  1. React技巧之检查元素是否可见

    原文链接:https://bobbyhadz.com/blog/react-check-if-element-in-viewport 作者:Borislav Hadzhiev 正文从这开始~ 总览 在 ...

  2. java常见的面试题(一)

    1.Collection 和 Collections 有什么区别? Collection 是一个集合接口(集合类的一个顶级接口).它提供了对集合对象进行基本操作的通用接口方法.Collection接口 ...

  3. python做小游戏——做个马里奥分分钟解决

    一.前言 嗨喽,大家好呀!这里是小熊猫 在你的童年记忆里,是否有一个蹦跳.顶蘑菇的小人已经被遗忘? 马里奥是靠吃蘑菇成长,闻名世界的超级巨星.特征是大鼻子.头戴帽子.身穿背带工作服.还留着胡子.帽子加 ...

  4. 分布式机器学习:模型平均MA与弹性平均EASGD(PySpark)

    计算机科学一大定律:许多看似过时的东西可能过一段时间又会以新的形式再次回归. 1 模型平均方法(MA) 1.1 算法描述与实现 我们在博客<分布式机器学习:同步并行SGD算法的实现与复杂度分析( ...

  5. Linux 磁盘挂载和swap空间管理

    挂载:把指定的设备和根下面的某个文件夹建立关联 卸载:解除两者关系的过程 挂载文件系统:mount 格式:mount device mountpoint --- mount 设备名 挂载点 mount ...

  6. 基于图的深度优先搜索策略(耿7.10)--------西工大noj

    ​ 代码 代码 #include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct ...

  7. .net6与英雄联盟邂逅之——根据官方LCU API制作游戏助手

    看了网上很多自己开发的英雄联盟的小助手工具,当时苦于没有api,自己也想做一个.后来发现了其实拳头本身就提供了LCU API在客户端运行的时候会暴露出来. 现在我们就来了解下工具的实现. 查询数据:h ...

  8. Random的概述和基本使用与生成指定范围的随机数

    Random类用来生成随机数字,使用起来需要三个步骤 1.导包 import java.util.Random; 2.创建 Random random = new Random();//小括号中留空即 ...

  9. 20220722-Java构造器

    Java构造器知识总结 来源:B站韩顺平老师Java入门教学 代码示例 class Person { int age; String name; public Person(int pAge, Str ...

  10. 项目开发中Maven的单向依赖-2022新项目

    一.业务场景 工作多年,在真实的项目开发中经常会遇到将一个项目拆分成多个工程的情况,比如将一个真实的项目拆分成controller层,service层, dao层,common公共服务层等等.这样拆分 ...