模板技术详解

news2025/1/16 21:41:54

目录

一、概念介绍

二、函数模板

2.1 概念

2.2 函数模板格式

2.3 函数模板原理

2.4 函数模板实例化

2.5 函数模板的匹配原则

三、类模板

3.1 类模板格式

3.2 类模板实例化

四、非类型模板参数

五、模板特化

5.1 概念

5.2 函数模板特化

5.3 类模板特化

六、模板编译分离

七、可变参数模板

八、模板总结


一、概念介绍

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。

模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,它们都使用了模板技术。

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础

二、函数模板

2.1 概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
 

2.2 函数模板格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}

注意:typename是用来定义模板参数的关键字,也可以使用class(切记:不能使用struct代替class)
 

2.3 函数模板原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定的具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演实例化生成对应类型的函数以供调用。
比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型等也是如此。

2.4 函数模板实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化

隐式实例化: 让编译器根据实参推演模板参数的实际类型
显式实例化: 在函数名后的<>中指定模板参数的实际类型

#include<iostream>
template<typename T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, a2);
	Add(d1, d2);
	//Add(a1, d1);//err
	/*
	该语句不能通过编译
	因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
	通过实参a1将T推演为int,通过实参d1将T推演为double类型,
    但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错
	注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
	*/

	//处理方式: 1.用户自己来强制转化 2.使用显式实例化 3.设置多个模板参数
	Add(a1, (int)d1);
	Add<int>(a1, d1);//显式实例化
	return 0;
}

2.5 函数模板的匹配原则

1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数(需显式指定)

#include <iostream>
using namespace std;
int Add(int left, int right)
{
	return left + right;
}
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}
int main()
{
	Add(1, 2);//与非模板函数匹配,编译器不需要实例化
	Add<int>(1, 2);//显示指定调用编译器实例化的版本
	return 0;
}

2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。
若模板可以产生一个具有更好匹配的函数, 那么将选择模板。

#include <iostream>
using namespace std;
int Add(int left, int right)
{
	return left + right;
}
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}
int main()
{
	Add(1, 2);//与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0);//模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
	return 0;
}

3.模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

三、类模板

3.1 类模板格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
    //类内成员定义
};

注意:类模板中函数放在类外进行定义时,需要加模板参数列表

3.2 类模板实例化

类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

//vector类名,vector<int>才是类型
Vector<int> s1;
Vector<double> s2;

四、非类型模板参数

模板参数分类类型形参与非类型形参。
类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:就是用一个常量作为模板的一个参数,在模板中可将该参数当成常量来使用

template<class T, size_t N = 10>
class array
{
public:
	//……
private:
	T _array[N];
	size_t _size;
};

注意:
1. 浮点数、类对象以及字符串不允许作为非类型模板参数
2. 非类型的模板参数必须在编译期就能确认结果

五、模板特化

5.1 概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。例如下面的代码。

#include<iostream>
using namespace std;
class Date
{
public:
	Date() :_year(0), _month(0), _day(0) {}
	Date(int year, int month, int day) :_year(year), _month(month), _day(day) {}
	bool operator<(const Date& d)const {
		if ((_year < d._year) || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && _day < d._day))
			return true;
		return false;
	}
private:
	int _year;
	int _month;
	int _day;
};
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误
	//这里本意比较Date类型数据的大小,实际比较的却是指针地址的大小
	return 0;
}

此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。

5.2 函数模板特化

1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同

#include<iostream>
using std::cout;
using std::endl;
class Date
{
public:
	Date() :_year(0), _month(0), _day(0) {}
	Date(int year, int month, int day) :_year(year), _month(month), _day(day) {}
	bool operator<(const Date& d)const {
		if ((_year < d._year) || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && _day < d._day))
			return true;
			return false;
	}
private:
	int _year;
	int _month;
	int _day;
};

template<class T>
bool Less(T left, T right)
{
	return left < right;
}
//对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}
int main()
{
	cout << Less(1, 2) << endl;
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl;
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl;//调用特化之后的版本,不进行模板生成
	return 0;
}

但是一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。

//该种实现简单明了,代码的可读性高,容易书写
//因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化
bool Less(Date* left, Date* right) {
	return *left < *right;
}

5.3 类模板特化

全特化: 全特化即是将模板参数列表中所有的参数都确定化

偏特化: 任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化有以下两种表现方式:
(1)部分特化:将模板参数类表中的一部分参数特化
(2)参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本

#include<iostream>
using std::cout;
using std::endl;
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
};

template<>//全特化
class Data<int, char>
{
public:
	Data() { cout << "Data<int, char>" << endl; }
};

template <class T1>//偏特化
class Data<T1, int>
{
public:
	Data() { cout << "Data<T1, int>" << endl; }
};
template<class T1, class T2>//偏特化
class Data<T1*,T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
};
template<class T1, class T2>//偏特化
class Data<T1&, T2&>
{
public:
	Data() { cout << "Data<T1&, T2&>" << endl; }
};

int main()
{
	Data<int, int> d1;//Data<T1, int>
	Data<int, char> d2;//Data<int, char>
	Data<double, double>d3;//Data<T1, T2>

	Data<int*, int*> d4;//Data<T1*, T2*>
	Data<double*, char*>d5;//Data<T1*, T2*>

	Data<double&, char&>d6;//Data<T1&, T2&>
	return 0;
}

六、模板编译分离

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

解决方法:

1. 将声明和定义放到一个文件 "xxx.hpp" 里或者"xxx.h"文件。
2. 模板定义的位置显式实例化。(不推荐使用,类型固定)

七、可变参数模板

C++11的新特性可变参数模板能创建可以接受可变参数的函数模板和类模板

//Args是一个模板参数包,args是一个函数形参参数包
template <class ...Args>
void ShowList(Args... args) {}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为"参数包",它里面包含了0到N(N>=0)个模版参数。

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

递归函数方式展开参数包

#include <iostream>
#include <string>
using namespace std;
template <class T>
void ShowList(const T& t)// 递归终止函数
{
	cout << "ShowList(" << t << ", " << 0 << "参数包)" << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(const T& value, Args... args)
{
	cout << "ShowList("<<value<<", " << sizeof...(args) << "参数包)" << endl;
	ShowList(args...);
}
int main()
{
	ShowList(1, 'A', std::string("sort"));
	return 0;
}
#include <iostream>
#include <string>
using namespace std;
//template <class T>
void ShowList()// 递归终止函数
{
	cout << endl;
}
template <class T, class ...Args>
void ShowList(const T& value, Args... args)
{
	cout << "ShowList("<<value<<", " << sizeof...(args) << "参数包)" << endl;
	ShowList(args...);
}
int main()
{
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的。
printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。
同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,
{(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),
最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。

由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包

#include <iostream>
#include <string>
using namespace std;
template<class T>
int PrintArg(const T& x)//处理每个参数的函数
{
	cout << x << " ";
	return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
	int a[] = { PrintArg(args)... };
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

八、模板总结

优点:
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
缺陷:
1. 模板会导致代码膨胀问题,也会导致编译时间变长
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/162134.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何使用mybatis处理数据库关系中的一对多关系呢?

测试环境的搭建&#xff1a; 本篇文章的测试环境搭建和上篇文章基本相似&#xff0c;这里在上篇文章传送门测试环境的基础上进行对比和修改&#xff01; 上篇文章所提到的多对一是多个学生对应一个老师&#xff0c;是在学生的角度去获取老师的信息&#xff0c;而本篇文章的一…

关于MySQL中的数据类型

一、常见的数据类型有&#xff1a; varchar&#xff08;最长255&#xff09;&#xff1a;【每个长度可以保存一个英文字符或一个汉字】 可变长度字符串 比较智能 节省空间 会根据实际的数据长度动态分配空间 优点&#xff1a;节省空间 缺点&#xff1a;需要动态分配空间&am…

phy-MDC时钟修改

问题分析:我们这边更换一种电平转换芯片&#xff0c;还是没调通。可能一个原因是这个芯片在开漏模式下速速最高到2M有关&#xff0c;您那边能帮忙协调一下&#xff0c;把内核PHY的MDC时钟改为2M以下&#xff0c;另把PHY的复位时间由现在的13MS左右调整到30MS左右我们试一下 在…

数据库知识学习

关系型数据库学习 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录关系型数据库学习一 数据库介绍1 相关定义数据&#xf…

基于卷积神经网络识别金融票据中的文字信息(计算机毕设完整代码可直接运行)

结果展示&#xff1a;用户首先通过”浏览文件”按钮选择扫描获得的金融票据图片. 程序就能够提取出金融票据图片中的日期, 金额等信息和图片路径信息显示在屏幕上. 程序还设置了帮助按键,使用者通过帮助按钮获得帮助.由图可见票据的日期为 19 年 06 月 22 日(062219), 程序可以…

CMMI之需求管理

需求管理&#xff08;Requirement Management, RM&#xff09;的目的在客户与开发方之间建立对需求的共同理解&#xff0c;维护需求与其他工作成果的一致性&#xff0c;并控制需求的变更。需求管理过程域是SPP模型的重要组成部分。本规范阐述了需求管理过程域的三个主要规程&am…

低代码平台是技术开发的未来主流吗?

2022年9月27日16:49:21 这个事情我想了很久&#xff0c;最终的结论是&#xff1a;低代码平台只会一些领域平台的部分功能模块的标配&#xff0c;不会成为技术主流。 部分代表性例子&#xff1a; 1&#xff0c;低代码平台很早就有了&#xff0c;比如Visual Basic 6.0 ,delphi这些…

电脑重装系统后每次都要选择系统怎么办

电脑重装系统是件非十分平常的事情&#xff0c;当电脑出现故障或卡顿不能解决&#xff0c;即可采取重装系统来修复。不过&#xff0c;有些用户遇到系统重装后每次开机都要选择系统&#xff0c;影响到电脑正常使用&#xff0c;想要解决这个问题&#xff0c;只要关闭引导配置就可…

【pandas】15 pandas数据结构

【pandas】15 pandas数据结构 2023.1.13 总结来自https://mofanpy.com/tutorials/data-manipulation/pandas/ 包括pandas数据结构Series/DataFrame;数据选取分类查询等内容15.1 为什么需要pandas 前面讲了numpy,我们发现&#xff0c;numpy主要用途就是对同一类数据进行处理&a…

多节点Linux环境打造

目录 一、环境准备 1. CentOS镜像下载 2. VMware下载 二、 安装操作系统 1. 配置虚拟机 2. 安装CentOS操作系统 3. 网络配置 4. 多节点打造 5. 节点网络互通 6. 关闭防火墙服务 7. 修改默认主机名 8. 关闭 SELinux 服务 9. 安装常用软件 一、环境准备 1. CentOS镜…

电脑重装系统后键盘失灵解决方法步骤

手提电脑重装系统后键盘失灵怎么办呢?众所周知,联想小新Air 13 Pro保持经典的黑色外观,凭借坚固和可靠的特性得到了很多用户的认可。有网友发现联想小新Air 13 Pro手提电脑重装系统后键盘会失灵&#xff0c;那么小编把手提电脑重装系统后键盘失灵解决方法分享给大家。 工具/原…

Vue声明式导航 编程式导航、导航守卫、axios拦截器

一、声明式导航 & 编程式导航 1. 声明式导航&#xff1a;以超链接方式实现的页面跳转&#xff0c;就是声明式导航 < a href‘url’> 链接文本或图像 < /a >< router-link to‘url’ > 链接文本或图像 < /router-link >2. 编程式导航&#xff1a;通…

录屏软件哪个好?电脑录屏软件排行榜推荐

你是不是还在为选不到合适的录屏软件而苦恼&#xff1f;市面上录屏软件种类繁多&#xff0c;功能参差不齐确实不好选择。录屏软件哪个好&#xff1f;怎样才能找到适合自己的录屏软件&#xff1f;不用焦虑。今天小编给大家推荐3款电脑录屏软件排行榜前列的录屏软件&#xff0c;每…

day37【代码随想录】贪心算法之划分字母区间、合并区间、单调递增的数字、买卖股票的最佳时机含手续费、监控二叉树

文章目录前言一、划分字母区间&#xff08;力扣763&#xff09;二、合并区间&#xff08;力扣56&#xff09;三、单调递增的数字&#xff08;力扣738&#xff09;四、买卖股票的最佳时机含手续费&#xff08;力扣714&#xff09;五、监控二叉树&#xff08;力扣968&#xff09;…

1.14 IIC总线实验

一.IIC总线&#xff1a; 1.同步半双工串行总线&#xff0c;用于同一个开发板两个芯片之间的通信 2.有两根信号线&#xff0c;一根SDA,一根SCL 3.IIC总线需要外接两个上拉电阻&#xff0c;使空闲状态保持高电平 4.IIC总线支持多主机多从机模式&#xff0c;一般采用单主机多从…

STM32外部中断解析

文章目录前言一、外部中断是什么二、STM32F103的外部中断三、外部中断的中断号四、HAL库的外部中断初始化流程总结前言 本篇文章将带大家了解STM32F103的外部中断。 一、外部中断是什么 外部中断是单片机实时地处理外部事件的一种内部机制。当某种外部事件发生时&#xff0c…

数组名的意义

数组名只有单独放在sizeof内部以及放在&后才代表整个数组的地址。其余情况数组名都表示数组首元素地址。 之前我们说过用sizeof(a)计算的是整个数组的大小&#xff0c;现在我们知道其中的原因了。由于sizeof里的数组名a表示整个数组的地址&#xff0c;故sizeof(a)求的是整…

Android10以上系统Audio音频遇到播放无声时的分析方法

​商务合作 2023年招聘 2023年逆向分析资料汇总 推荐阅读 Android Audio音频系统 Android Audio音频系统之深入浅出 Android Framework/驱动/内核中高级工程师 ​Android10以上系统Audio音频遇到播放视频无声时的分析方法 干货|Android APP应用工程师转Framework工程师(…

51单片机存储结构

之前概要介绍了8151微控制器的结构&#xff08;也就是51单片机&#xff09;。相比微处理器&#xff0c;微控制器的区别之一是在一个芯片上有程序存储器(RAM)和数据存储器(RAM)。存储区是微控制器非常重要的内容。 本文就介绍一下8051的存储结构。包括存储器的组织、处理器对存储…

VTK-Tessellator Subdivision

前言&#xff1a;本博文主要研究Tessellator 的Subdivision&#xff0c;对vtk中的所有相关接口进行研究&#xff0c;并找出最优的解决方法。 GeometricObjects中vtkTessellatorFilter的应用实例 待研究对当前的Subdivision进行优化。 vtkTessellatorFilter 位置&#xff1a;…