【C++杂货铺】C++11新特性——可变参数模板

news2024/11/25 11:59:27

在这里插入图片描述

文章目录

  • 一、可变模板参数相关概念的引入
  • 二、获取参数包中参数的个数
  • 三、递归函数方式展开参数包
  • 四、逗号表达式展开参数包
  • 五、可变模板参数的实际应用——emplace相关接口
    • 5.1 回顾一下 push_back 的三种用法
    • 5.2 emplace_back 使用方法介绍
    • 5.3 听说 emplace_back 可以提高效率?
  • 六、结语

一、可变模板参数相关概念的引入

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模板和函数模板中只能含固定数量的模板参数,可变模板参数无疑是一个巨大的改进。然而由于可变模板参数比较抽象,使用起来需要一定的技巧,所以之一块还是比较晦涩的。本篇文章旨在帮助大家掌握一些基础的可变参数特性,足够大家使用。

相信大家对可变参数这一概念并不陌生,在 C语言阶段我们常用的 scanfprintf 它们就使用了可变参数,但它们属于函数的可变参数,和我们今天所要讲解的模板的可变参数有所不同。函数的参数传递的是对象,而模板的参数传递的是类型(非类型的模板参数除外),函数的可变参数是希望传递任意个数的对象,那模板的可变参数就是希望传递任意个数的类型。下面就是一个基本可变参数的函数模板。

template<class ...Args>
void ShowList(Args... args)
{}

其中 Args 是一个模板参数包,args 是一个函数形参参数包。声明一个参数包 Args... args,这个参数包中可以包含 0 到任意个模板参数。参数 args 前面有省略号,所以它就是一个可变模板参数,我们把带省略号的参数称为“参数包”,它里面包含了 0 到 N (N>=0)各模板参数。我们无法直接获取参数包 args 中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模板参数的一个主要特点,也是最大的难点,即如何展开可变模板参数。由于语法不支持使用 args[i] 这样的方式获取可变参数,所以我们得用一些奇招来一一获取参数包的值。

二、获取参数包中参数的个数

template<class ...Args>
void ShowList(Args... args)
{
	cout << sizeof...(args) << endl; // 查看参数包中的参数个数
}

int main()
{
	ShowList(1);
	ShowList(1, 1.1);
	ShowList(1, 1.1, 'a');
	return 0;
}

在这里插入图片描述
可以通过 sizeof...(args) 来查看参数包中的参数个数。

三、递归函数方式展开参数包

//递归终止函数
template<class T>
void ShowList(T val)
{
	cout << val << endl;
}
// 可变模板参数
template<class T, class ...Args>
void ShowList(T val, Args... args)
{
	cout << val << " ";
	ShowList(args...);
}

int main()
{
	ShowList(1);
	ShowList(1, 2.1);
	ShowList(1, 2.1, 'a');
	return 0;
}

在这里插入图片描述
该方法是通过递归调用 ShowList 函数去获取参数包中的参数,每递归一次就可以从参数包中取出一个参数存到形参 val 中。注意:采用这种方法获取参数包中的参数必须要重载一个仅有一个参数的同名函数,也就是递归终止函数。假如不写这个函数会出现什么问题呢?问题出现在当参数包中只有一个参数的时候,如果参数包中只剩一个参数,此时执行 ShowList(args...); ,可以调用 void ShowList(T val, Args... args) 没有任何问题,将参数包中仅存的一个参数传给第一个形参 val,此时形参 args (参数包)中没有任何东西,然后再去递归调用 ShowList(args...); 这时问题就来了,因为此时的 args 中什么都没有,所以就相当于无参调用 ShowList();,但是我们并没有重载 ShoeList 同名的无参函数,所以就会报错。当我们写了上面的递归终止函数就不会出现这样的问题,因为上面的递归终止函数中只有一个形参,因此当参数包中只剩一个参数的时候, ShowList(args...); 会去走最匹配的,也就是去调用我们写的递归终止函数,这样就可以把参数包中的最后一个参数提取出来,并且结束掉递归。通过上面的分析,我们可以得出,递归终止函数也可以重载成一个无参的同名函数,像下面这样:

// 递归终止函数
void ShowList()
{
	cout << endl;
}
// 可变模板参数
template<class T, class ...Args>
void ShowList(T val, Args... args)
{
	cout << val << " ";
	ShowList(args...);
}

int main()
{
	ShowList(1);
	ShowList(1, 2.1);
	ShowList(1, 2.1, 'a');
	return 0;
}

在这里插入图片描述

四、逗号表达式展开参数包

template<class T>
void PrintArg(T t)
{
	cout << t << " ";
}

// 可变模板参数
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}

int main()
{
	ShowList(1);
	ShowList(1, 2.1);
	ShowList(1, 2.1, 'a');
	return 0;
}

在这里插入图片描述
这种展开参数包的方式,不需要通过递归终止函数,是直接在 ShowList 函数体中展开的,PrintArg 不是递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。逗号表达式会按顺序执行逗号前面的表达式。ShowList 函数中的逗号表达式:(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 数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程中展开参数包。

五、可变模板参数的实际应用——emplace相关接口

5.1 回顾一下 push_back 的三种用法

下面我们将采用 list 容器去探究 push_backemplace_back 的用法与差异。list 中存的是 pair<int, char> 类型的对象。

  • 定义一个存储 pair<int, char> 类型对象的链表
std::list< std::pair<int, char> > mylist;
  • 方式一
std::pair<int, char> pa(1, 'a');
mylist.push_back(pa);

这种方式是最初阶的玩法,先定义一个 pair<int, char> 类型的对象 pa,此时会调用 pair 的构造函数。然后再将对象 pa 插入链表中。因为 pa 是一个左值,所以最终会调用左值引用版本的插入,即调用:void push_back (const value_type& val);

在这里插入图片描述

  • 方式二
mylist.push_back(wcy::make_pair(2, 'b'));
mylist.push_back(wcy::pair<int, char>(3, 'c'));

方式二是先调用 make_pair 函数创建一个 pair<int, char> 类型的对象,然后直接将 make_pair 函数的返回值插入到链表中,因为函数的返回值会被当做右值,所以这里最终会去调用右值引用版本的插入。即:void push_back (value_type&& val);。直接创建匿名对象进行插入的函数调用链和使用 make_pair 函数进行插入的函数调用链是一样的,因为匿名对象的生命周期就只有一行,编译器会把它识别成右值中的将亡值,因此把这两种方式归为一类。
在这里插入图片描述

  • 方式三
mylist.push_back({ 4, 'd' });

方式三的插入方式是 C++11 新增的,{4, d} 会去调用 pair 的列表初始化创建出一个 pair<int, char> 类型的对象。列表初始化本质上是 C++11 允许多参数的构造函数支持隐式类型的转化。使用列表初始化创建出来的对象生命周期也只有一行,会被编译器识别成右值,因此最终回去调用右值版本的插入,其函数调用链和方式二是一样的。

5.2 emplace_back 使用方法介绍

上面介绍的是 push_back 的使用方法,下面来介绍 emplace_back 的使用方法。

template <class... Args>
void emplace_back (Args&&... args);

emplace_backpush_back 最大的不同就在于它的参数采用了可变模板参数,这就决定了它可以接受各种类型的参数,而 push_back 的参数类型是固定的,只能是 pair<int, char> 类型,即链表中要存储的数据类型,这在链表创建的初期就已经被确定下来了。由于 emplace_back 采用的是可变模板参数,因此 push_back 的三种使用方式也同样适用于 emplace_back 这里就不再过多赘述,这里主要想给大家分享一下 emplace_back 新增的一种使用方法。

mylist.emplace_back(5, 'e');

要想搞懂 emplace——back 的原理,我们需要先理解下面这段代码:

class Date
{
	friend std::ostream& operator<<(std::ostream& out, const Date* date);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

private:
	int _year;
	int _month;
	int _day;
};

std::ostream& operator<<(std::ostream& out, const Date* date)
{
	out << date->_year << " 年 " << date->_month << " 月 " << date->_day << " 日 " << endl;
	return out;
}

template<class...Args>
Date* CreatDate(Args...args)
{
	Date* date = new Date(args...);
	return date;
}

int main()
{
	Date* p1 = CreatDate();
	Date* p2 = CreatDate(2023);
	Date* p3 = CreatDate(2023, 12);
	Date* p4 = CreatDate(2023, 12, 30);
	Date* p5 = CreatDate(*p3);// 最终是去调用拷贝构造

	cout << p1 << p2 << p3 << p4 << p5;
	return 0;
}

在这里插入图片描述

上面代码可以分为三个部分:日期类、CreatDate函数、主函数。这里创建日期类对象是通过 CreatDate 函数来实现的。该函数使用了可变模板参数,这样我们在主函数中调用 CreatDate 函数时可以传递任意个数的参数来创建 Date 类对象。new Date(args...) 最终是通过参数包的类型去调用构造函数或者拷贝构造函数。

mylist.emplace_back(5, 'e'); 的原理和上面的逻辑是一致的,就是将 (5, 'e') 放在一个参数包里,一层层的往下传递,最终还是去调用 pair 的普通构造函数。

在这里插入图片描述

5.3 听说 emplace_back 可以提高效率?

首先说明,所有的提高效率一般都是针对需要深拷贝的对象来说的,提高效率就是减少深拷贝的次数。因上面的实验,在 list 中存的是 pair<int, char> 类型对象,这里不涉及深拷贝,因此无法证明 emplace_back 可以提高效率。因此,这里我们对 list 存储的对象类型进行修改,让它存储一个需要进行深拷贝的对象 即 pair<int, string> 类型的对象。

int main()
{
	// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
	// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
	// 是先构造,再移动构造,其实也还好。
	std::list< std::pair<int, wcy::string> > mylist;
	mylist.emplace_back(10, "sort");
	cout << "=========================" << endl;
	mylist.emplace_back(std::make_pair(20, "sort"));
	cout << "=========================" << endl;
	mylist.push_back(std::make_pair(30, "sort"));
	cout << "=========================" << endl;
	mylist.push_back({ 40, "sort" });
	return 0;
}

在这里插入图片描述
通过上面这段代码的执行结果可以看出,使用 emplace_bakce 进行插入的时候,对于需要深拷贝的对象,它会将参数包一层层的往下传,最终只调用一次普通的构造函数。而使用 push_back 进行插入的时候,会先执行一次普通构造,再调用一次移动构造。push_bakc 过程中调用普通构造是因为,push_back 函数的参数在链表创建后就是固定的,以上面的代码为例,它的 push_back 函数的参数一定是 pair<int, wcy::string> 类型的对象引用(可以是左值引用也可以是右值引用)。因此首先需要创建一个 pair<int, string> 类型的对象作为实参。其中:std::make_pair(30, "sort"){ 40, "sort" } 就是去调用普通的构造函数创建对象作为实参,通过这两条语句创建的对象叫做临时对象,因为它的生命周期就只有一行,所以这两条语句创建出来的对象会被编译器识别成右值,最终去调用右值版本的插入。在右值版本的插入过程中会执行 new Node(forward<T>(val)) 去创建节点,移动构造就是在创建节点的时候去调用的。emplace_back 可以看作只在创建节点的时候调用了一次构造函数。通过前面的分析可以看出,其实 emplace_back 并没有提高多少效率,因为 push_back 使用移动构造的代价已经足够低了。移动构造中就是进行资源的置换,一般就是指针的交换,代价并不是很大。

小Tips:总结一下,对于需要进行深拷贝的对象来说,emplace_backpush_back 的差距并不大。但是对于一个非常非常大的需要浅拷贝的对象来说,因为浅拷贝的对象一般都不会自己去写拷贝构造和移动构造,而是直接使用编译器默认生成的,这种情况下编译器默认生成的都是完成浅拷贝,那使用 push_back 会先调用一次构造再调用一次拷贝构造,前后创建了两个大对象,而 emplace_back 只会调用一次构造,只创建一个大对象。需要注意,前面说的这些都是建立在按照方式二或者方式三的方法或者使用 emplace_back 特有的方法去进行插入。

六、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

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

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

相关文章

SCT2601,可替代LMR16006X/MP2459/MP2456;4.5V-60V Vin,0.6A,高效降压DCDC转换器

•宽输入范围&#xff1a;4.5V-60V •高达0.6A的连续输出电流 •0.765V2.5%反馈参考电压 •集成500mΩ高压侧MOSFET •低静态电流为80uA •轻负载下的脉冲跳过模式&#xff08;PSM&#xff09; •最小接通时间80ns •内置6ms软启动时间 •开关频率为700KHz •可编程输入电压欠压…

探秘AI数字人克隆系统OEM源码:实现24小时无人值守直播间的奥秘

随着人工智能技术的不断发展&#xff0c;AI数字人克隆系统OEM源码正在引起广泛的关注。其中&#xff0c;实现24小时无人值守直播间成为了许多企业和机构的追求。本文将深入探讨如何利用AI数字人克隆系统OEM源码实现24小时无人值守直播间&#xff0c;并揭示其背后的奥秘。 一、…

Acrel-2000MG工商业储能站能量管理系统-光伏储能一体化解决方案

安科瑞 崔丽洁 Acrel-2000MG储能能量管理系统是安科瑞专门针对工商业储能电站研制的本地化能量管理系统&#xff0c;可实现了储能电站的数据采集、数据处理、数据存储、数据查询与分析、可视化监控、报警管理、统计报表、策略管理、历史曲线等功能。其中策略管理&#xff0c;支…

【活动回顾】ABeam 德硕 | 企业座谈及宣讲会--石河子大学专场

ABeam 石河子大学校招之旅 沙漠绿洲&#xff0c;戈壁明珠 近日&#xff0c;ABeam大中华区董事长兼总经理中野洋辅先生带领西安招聘团队来到新疆维吾尔自治区石河子市——石河子大学&#xff0c;与信息科学与技术学院领导就校企合作事宜进行了深入的座谈交流&#xff0c;并在随…

【Unity入门】UGUI之Slider(滑动条)

目录 一、什么是Slider&#xff1f;二、Slider属性与功能 一、什么是Slider&#xff1f; Slider控件允许用户可以通过鼠标来在预先确定的范围调节数值 我们可以在Hierarchy视图右键 -> UI ->Slider来创建滑动条 通过上图可以发现Unity内置的Slider主要有3部分&#x…

【web】vue 播放后端(flask)发送的 mp3 文件

文章目录 演示后端&#xff08;flask&#xff09;前端&#xff08;vue3&#xff09;重要说明 演示 后端&#xff08;flask&#xff09; 后端返回的是 mp3 文件的 url&#xff0c;是可以直接在浏览器上打开后播放的处理跨域请求pip install flask-cors后端代码from flask impor…

一场高规格品鉴会,窥探剑南春冲击高端的“野心”

执笔 | 洪大大 编辑 | 扬 灵 12月27日&#xff0c;以“锦绣巴蜀品味东方”为主题的剑南春东方红致敬中国时代品鉴会在有云鹿洄天府1911中国川菜体验中心举办&#xff0c;数十位媒体代表和嘉宾汇聚一堂&#xff0c;共同品味剑南春东方红的品质魅力&#xff0c;感知高端白酒品…

什么是缓存、为什么要用缓存、缓存分类、缓存测试、缓存更新、缓存设计考虑点、缓存测试点

一、缓存 缓存是一种将数据存储在高速缓存中的技术&#xff0c;它可以提高应用程序的性能和响应速度。 二、 为什么要用缓存 1. 高性能(主要目的) 查询耗时&#xff0c;但变化少&#xff0c;又有很多读请求情况下&#xff0c;可以将查询结果放到缓存中。减少对数据库的压力&…

光伏、储能一体化监控及运维解决方案 安科瑞 许敏

前言&#xff1a;今年以来&#xff0c;在政策利好推动下光伏、风力发电、电化学储能及抽水蓄能等新能源行业发展迅速&#xff0c;装机容量均大幅度增长&#xff0c;新能源发电已经成为新型电力系统重要的组成部分&#xff0c;同时这也导致新型电力系统比传统的电力系统更为复杂…

clang-format

Clang-Format Clang-Format Style Options — Clang 18.0.0git documentation VSCode 1.1 安装扩展 C 1.2 设置 1.3 使用 .clang-fornat 放置在项目&#xff08;代码&#xff09;文件夹下使用 .clang-fornat 为文件名 --- # https://clang.llvm.org/docs/ClangFormatStyle…

document360的替代品:原来它也这么好用

在当今信息爆炸的时代&#xff0c;企业和组织需要一个高效的知识管理系统来整理、存储和共享知识。document360是一款备受欢迎的知识库管理系统&#xff0c;但是它并非唯一的选择。本文将介绍document360的替代品&#xff0c;感兴趣就往下看吧。 首先&#xff0c;让我们来了解…

Python分析了京东7万+条评论,仅用3分钟,结果发现……

之前学习Python爬虫采集&#xff0c;为了练手用Scrapy写了一个爬虫&#xff0c;整整采集了京东平台vivo旗舰店7万多条评论。一直也没觉得这些评论数据有啥用&#xff0c;就留在MongoDB中吃灰。最近学了jieba和wordcloud之后&#xff0c;突发奇想着分析下这7万多条评论数据&…

【网络安全】上网行为代理服务器启用Alerts

文章目录 启用AlertsAlert Limits per 24 hoursEmail AlertsSystem Alerts Suspicious Activity AlertsPermitted Suspicious Activity AlertsBlocked Suspicious Activity Alerts Protocol&Category Usage Alerts告警邮件范例推荐阅读 这里的Web Proxy主要代指proxy serve…

HarmonyOS应用开发-搭建开发环境

本文介绍如何搭建 HarmonyOS 应用的开发环境&#xff0c;介绍下载安装 DevEco Studio 开发工具和 SDK 的详细流程。华为鸿蒙 DevEco Studio 是面向全场景的一站式集成开发环境&#xff0c;面向全场景多设备&#xff0c;提供一站式的分布式应用开发平台&#xff0c;支持分布式多…

声纹识别资源汇总(不断更新)

目录 一、任务说明二、指标三、声纹识别研究现状四、数据集开源&#xff08;1&#xff09;VoxCeleb&#xff1a;&#xff08;2&#xff09;WSJ and LibriSpeech Corpus&#xff08;3&#xff09;VOiCES Dataset&#xff08;4&#xff09;English Multi-speaker Corpus for Voic…

机器学习笔记(四)初识卷积神经网络

前言 第一次写卷积神经网络&#xff0c;也是照着paddlepaddle的官方文档抄&#xff0c;这里简单讲解一下心得。 首先我们要知道之前写的那些东西都是什么&#xff0c;之前写的我们称之为简单神经网络&#xff0c;也就是简单一层连接输出和输出&#xff0c;通过前向计算和逆向…

LM358 典型应用Multisim仿真设计

一、LM358简介&#xff1a; LM358 运算放大器属于一种低功率双运算放大器&#xff0c;由两个独立的高增益内部频率补偿运算放大器组成&#xff0c;专门设计用于在宽电压范围内由单电源供电。LM358 运算放大器具有低功耗、共模输入电压范围扩展到地/VEE以及单电源或双电源操作。…

代表团坐车 - 华为OD统一考试

OD统一考试&#xff08;B卷&#xff09; 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 某组织举行会议&#xff0c;来了多个代表团同时到达&#xff0c;接待处只有一辆汽车可以同时接待多个代表团&#xff0c;为了提高车辆利用率&#xff0c;请帮接待…

Linux 系统拉取 Github项目

一、安装Git 在Linux上拉取GitHub项目可以使用Git命令。首先确保已经安装了Git。如果没有安装&#xff0c;可以通过包管理器&#xff08;比如apt、yum&#xff09;来进行安装。 sudo yum install git #查看安装版本 git -version二、关联GitHub 配置本地账户和邮箱 >>…

Redis缓存穿透,缓存击穿,缓存雪崩

文章目录 Redis缓存穿透&#xff0c;缓存击穿&#xff0c;缓存雪崩1. 缓存穿透1.1 解决方案1&#xff1a;缓存空数据1.2 解决方案2&#xff1a;使用布隆过滤器1.2.1 布隆过滤器介绍 2. 缓存击穿2.1 解决方案1&#xff1a;互斥锁2.2 解决方案2&#xff1a;逻辑过期 3. 缓存雪崩3…