C++11——2:可变模板参数

一.前言

C++11引入了可变模板参数(variadic template parameters)的概念,它允许我们在模板定义中使用可变数量的参数。这样,我们就可以处理任意数量的参数,而不仅限于固定数量的参数。

二.可变模板参数

我们早在C语言的学习过程中就接触过可变参数:printf和scanf都是可变参数的函数

他们的参数可以是1个,也可以是多个。其底层使用了一个二维数组将所有的参数存储起来。

对于之前的模板来说,都是固定参数个数的模板。C++11引入了可变参数的模板,即不仅参数的类型是不确定的,个数也是不确定的。支持可变参数的函数模板和类模板可变数目的参数被称为参数包,而参数包又分为两种:模板参数包和函数参数包

模板参数包:表示0~n个模板参数;函数参数包:表示0~n个函数参数。

template <typename ...Args> // 模板参数包
void Func(Args ...args) // 函数参数包
{}// 左值引用
template <typename ...Args> // 模板参数包
void Func(Args& ...args) // 函数参数包
{}// 万能引用——引用折叠
template <typename ...Args> // 模板参数包
void Func(Args&& ...args) // 函数参数包
{}

可变参数模板的语法规范是在定义模板参数列表的时候typename/class后面跟三个点...,然后写上模板参数名,这个名字是可以随便写的,但是为了规范使用Args更好一些。在定义函数形参表的时候类型名后面跟三个点...然后加上对象名。

函数参数包可以传值,传左值引用或者右值引用,跟普通的模板一样,这里也遵循引用折叠规则。

template <typename ...Args>
void Print(Args...args)
{cout << sizeof...(args) << endl;
}int main()
{Print();Print(1);Print(1,3.14);Print(1,"1111111111",3,14);return 0;
}

对于上面这个Print函数,它支持可变模板参数,我们可以传0~n任意不同的参数。sizeof...可以计算参数包中的参数个数

那么我们在调用Print函数的时候到底发生了什么呢? 

编译器首先会根据我们调用该函数的实参的个数,生成有固定参数的函数模板,然后再根据实参的类型,实例化这些函数模板。

我们也可以反过来想一下:

阶段一:

        当没有模板语法的时候,我们想要实现可变参数的Print函数时,就要写多个函数,0个、1个、2个,n个,再根据实参类型不同,1个参数的函数也要写多个以适应不同的实参,2个、3个、n个的也一样。就很烦~

阶段二:

        当模板出来之后,我们不再需要写那些含有相同参数个数但参数类型不同的函数了,直接一个模板就可以了。这样对于1个参数的函数模板来说,任意类型的参数都可以传了,方便了很多。但是因为参数个数的不同,我们还是需要写很多个参数不同的函数模板。还是很烦~

阶段三:

        当可变模板参数出来了之后,我们也不再需要写多个参数个数不同的函数模板了,直接写一个可变参数的函数模板,他既解决了参数个数的不同,也解决了参数类型的不同。

而且,实参传的是左值还是右值也会影响实际生成的函数的样子,所以可变模板参数确实很方便。

虽然我们是这样分析可变模板参数调用的过程,但是在编译的过程中,编译器可能就不会有中间这步生成固定参数的函数模板,而是直接一步到位生成最终要调用的函数。

三.包扩展

对于一个参数包来说,我们除了计算它的参数个数之外,更多的情况下我们要去使用它,那么我们要怎么使用呢?我们能否在该函数内部直接使用for循环遍历该参数包呢?

当然是不行的了. 

 下面我们介绍两种包扩展的方式:

1.编译时递归包扩展

void ShowList()
{cout << endl;
}template<typename T, typename...Args>
void ShowList(T&& x, Args&&...args)
{cout << x << endl;ShowList(args...);
}template <typename ...Args>
void Print(Args&&...args)
{ShowList(args...);
}

我们通过这两个ShowList函数来实现包扩展。当我们在主函数调用Print函数的时候,实参会形成一个参数包,传给Print函数,Print函数内部无法直接解析包的内容,我们将参数包传给另一个函数,该函数也是一个函数模板,他会把参数包进行拆分:提取出参数包的第一个参数,然后打印该参数,然后自己调用自己将其他的参数作为参数包继续进行拆分。当参数包只剩一个参数的时候,T获取该参数,参数包变成空包,此时就会匹配那么无参数的ShowList,此时包扩展的过程完成。

由上述过程可以看出来,无参的ShowList起到了终止条件的作用。

该过程底层就类似于下图这个过程:

我们在调用Print函数的时候,编译器就会生成右边这几个函数。

那么我们是否可以将这个结束条件写道有参数的ShowList内部呢?

不行!

我们需要注意,我们上述包扩展的过程是编译时完成的,编译阶段,编译器对参数进行解析,生成若干个重载函数,实际上我们是调用那些重载函数来完成操作的。

而上图中if判断则是运行时逻辑,在程序运行的过程中,当参数包为空,递归结束。但是实际上,当参数包为空后,此时ShowList调用不到该函数本身,因为该函数本身必须接受一个参数T。

2. 通过返回值做函数参数包扩展

template<typename T>
const T& getArg(T&& x)
{cout << x << endl;return x;
}template<typename...Args>
void Arguments(Args...args)
{}template <typename ...Args>
void Print(Args&&...args)
{Arguments(getArg(args)...);
}

上面这种办法,我们在Print函数内部调用Arguments函数,但是调用该函数我们就得有实参,所以程序会先求该函数的实参,而实参是通过getArg函数解析参数包来获取的,所以我们也可以通过这种方式进行包扩展。

因为参数传参是从右往左的,所以这样解析参数包是反着解析的

四.emplace系列接口

1.emplace和push系列的对比

C++11引入了可变模板参数之后,STL的容器都引入emplace这一系列的接口。该系列的接口使用了可变模板参数,可以传任意类型和任意数量的参数。

那么emplace_back和push_back有什么区别呢? 我们接下来根据插入数据的类别来进行对比:

插入左值,push_back和empalce_back一样,都是拷贝构造。

插入左值push_back和emplace_back会进行引用折叠,最后都会调用左值引用版本,最后在函数内部,会new一个新节点,此时传给data的是一个左值引用,而data的类型是string,所以调用string的拷贝构造

插入右值,push_back和emplace_back一样,都调用移动构造。

插入右值,不会发生引用折叠,就会匹配对应的右值引用版本,在该函数内部,new一个新节点的时候,传给data的是一个右值,就是匹配data的移动构造

传匿名对象,push_back走隐式类型转换,先构造出临时对象,在函数内部引用该临时对象的调用移动构造;而emplace_back直接构造 

emplace_back之所以会直接构造,是因为emplace_back的参数类型是不确定的,只有当传参的时候才知道是const char*,所以在函数内部会直接调用string的构造函数来生成对象,list节点存储该对象。

当插入的是多参数的时候,规律和上面是一样的:

当插入左值时,两者都是拷贝构造;插入右值时,两者都是移动构造;插入匿名对象的时候,emplace_back是直接构造,而push_back是构造+移动构造

综上,在大部分情况下,push_back和emplace_back效率是一样的。但是在有些情况下,emplace_back直接构造,而push_back需要构造+移动构造(深拷贝的类型)或者构造+拷贝构造(浅拷贝的类型)。虽然移动构造的消耗很小,但毕竟有消耗;emplace_back和push_back真正的差异在于浅拷贝的类型,浅拷贝的类型没有移动构造,只能拷贝构造。

总的来说,emplace_back的效率整体上还是优于push_back的,所以以后用emplace系列的接口来替代push系列和insert系列的接口。

2.emplace的模拟实现

emplace_back就是可变模板参数类型的函数。但是在函数内部,我们没有必要对参数包进行扩展,我们并不在函数内使用参数,所以我们直接向下一层传递包即可。

template<typename...Args>
void emplace_back(Args...args)
{insert(end(), std::forward<Args>(args)...); // 直接向下转递参数包
}


完~ 

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

君正T41交叉编译ffmpeg、opencv并做h264软解,利用君正SDK做h264硬件编码

目录 1 交叉编译ffmpeg----错误解决过程&#xff0c;不要看 1.1 下载源码 1.2 配置 1.3 编译 安装 1.3.1 报错&#xff1a;libavfilter/libavfilter.so: undefined reference to fminf 1.3.2 报错&#xff1a;error: unknown type name HEVCContext; did you mean HEVCPr…

感知器的那些事

感知器的那些事 历史背景Rosenblatt和Minsky关于感知机的争论弗兰克罗森布拉特简介提出感知器算法Mark I感知机争议与分歧马文明斯基简介单层感知器工作原理训练过程多层感知器工作原理单层感知机 vs 多层感知机感知器模型(Perceptron),是由心理学家Frank Rosenblatt在1957年…

C语言:枚举类型

一、枚举类型的声明 枚举顾名思义就是一一列举。我们可以把可能的取值一一列举。比如我们现实生活中&#xff1a; 星期一到星期日是有限的7天&#xff0c;可以一一列举 &#xff1b;性别有&#xff1a;男、女、保密&#xff0c;也可以一一列举 &#xff1b;月份有12个月&#x…

25/1/6 算法笔记<强化学习> 初玩V-REP

我们安装V-REP之后&#xff0c;使用的是下面Git克隆的项目。 git clone https://github.com/deep-reinforcement-learning_book/Chapter16-Robot-Learning-in-Simulation.git 项目中直接组装好了一个机械臂。 我们先来分析下它的对象树 DefaultCamera:摄像机&#xff0c;用于…

CODESYS MODBUS TCP通信(AM400PLC作为主站通信)

禾川Q1 PLC MODBUS-TCP通信 禾川Q1 PLC MODBUS-TCP通信(CODESYS平台完整配置+代码)-CSDN博客文章浏览阅读17次。MATLAB和S7-1200PLC水箱液位高度PID控制联合仿真(MODBUSTCP通信)_将matlab仿真导入plc-CSDN博客文章浏览阅读722次。本文详细介绍了如何使用MATLAB与S7-1200PLC进行…

OSPF - 影响OSPF邻居建立的因素

总结为这么10种 routerID 冲突区域id不一致认证MA网络掩码需一致区域类型(特殊区域)hello、dead时间MTU(如果开启检查)静默接口网络类型不匹配MA网络中路由器接口优先级全为0 如何建立邻居可以查看上一篇文章&#xff0c;可以直接专栏找&#xff08;&#x1f92b;挂链接会没流…

【大数据】(选修)实验4 安装熟悉HBase数据库并实践

实验4 安装熟悉HBase数据库并实践 1、实验目的 (1)理解HBase在Hadoop体系结构中的角色; (2)熟练使用HBase操作常用的Shell命令; (3)熟悉HBase操作常用的Java API。 2、实验平台 操作系统:Linux Hadoop版本:2.6.0或以上版本 HBase版本:1.1.2或以上版本 JDK版…

windeployqt.exe打包qt程序总结(MSVC)

文章目录 前言打包步骤问题 前言 打包环境&#xff1a;windows10VS2017QT5.12.12 参考&#xff1a;Qt 打包发布程序&#xff0c;解决找不到msvcp140.dll等动态库问题正确方案 打包步骤 运行Qt5.12.12&#xff08;MSVC 2017 64-bits&#xff09; 在开始软件菜单里找到Qt文件夹…

算法的学习笔记—不用常规控制语句求 1 到 n 的和

&#x1f600;前言 在算法编程中&#xff0c;有时我们会遇到一些特殊的限制条件&#xff0c;这些限制会迫使我们跳出常规思维。本文讨论的问题就是一个典型案例&#xff1a;在不能使用基本控制语句的情况下&#xff0c;如何求解 1 到 n 的和。这个问题不仅考验编程技巧&#xf…

计算机网络 (27)IP多播

前言 IP多播&#xff08;也称多址广播或组播&#xff09;技术是一种允许一台或多台主机&#xff08;多播源&#xff09;发送单一数据包到多台主机&#xff08;一次性的、同时的&#xff09;的TCP/IP网络技术。 一、基本概念 定义&#xff1a;多播作为一点对多点的通信&#xff…

CSS 学习之正确看待 CSS 世界里的 margin 合并

一、什么是 margin 合并 块级元素的上外边距(margin-top)与下外边距(margin-bottom)有时会合并为单个外边距&#xff0c;这样的现象称为“margin 合并”。从此定义上&#xff0c;我们可以捕获两点重要的信息。 块级元素&#xff0c;但不包括浮动和绝对定位元素&#xff0c;尽…

小程序组件 —— 28 组件案例 - 推荐商品区域 - 实现结构样式

这一节目标是实现底部推荐商品的结构和样式&#xff0c;由于这里要求横向滚动&#xff0c;所以需要使用上节介绍的 scroll-view 功能&#xff0c;并使用 scroll-x 属性支持横向滚动&#xff0c;推荐商品区域中的每一个商品是一个单独的 view&#xff0c;每个view 中需要写三个组…

单片机-LED点阵实验

要将第一个点点亮&#xff0c;则 1 脚接高电平 a 脚接低电平&#xff0c;则第一个点就亮了&#xff1b;如果要将第一行点亮&#xff0c;则第 1 脚要接高电平&#xff0c;而&#xff08;a、b、c、d、e、f、g、h &#xff09;这些引脚接低电平&#xff0c;那么第一行就会点亮&…

软件项目体系建设文档,项目开发实施运维,审计,安全体系建设,验收交付,售前资料(word原件)

软件系统实施标准化流程设计至关重要&#xff0c;因为它能确保开发、测试、部署及维护等各阶段高效有序进行。标准化流程能减少人为错误&#xff0c;提升代码质量和系统稳定性。同时&#xff0c;它促进了团队成员间的沟通与协作&#xff0c;确保项目按时交付。此外&#xff0c;…

Java基础 注解

分类 Java自带的标准注解&#xff0c;包括Override、Deprecated和SuppressWarnings&#xff0c;分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告&#xff0c;用这些注解标明后编译器就会进行检查。元注解&#xff0c;元注解是用于定义注解的注解&#xff0…

Linux中rsync命令使用

一、rsync简介 rsync 是一种高效的文件复制和同步工具&#xff0c;常用于在本地或远程计算机之间同步文件和目录 主要特性增量同步&#xff1a;rsync 会检测源和目标文件之间的差异&#xff0c;只传输发生变化的部分&#xff0c;而不是重新传输整个文件。这样就能有效减少数据…

基于STM32的自动水满报警系统设计

目录 引言系统设计 硬件设计软件设计系统功能模块 水位检测模块报警模块自动控制模块控制算法 水位检测逻辑报警触发逻辑代码实现 水位检测模块报警控制模块自动控制逻辑系统调试与优化结论与展望 1. 引言 水满报警系统在家庭、农业、工业等领域广泛应用&#xff0c;通过实时…

【Java数据结构】二叉树

1.树型结构 1.1树的概念 树是一种非线性的数据结构&#xff0c;由n个结点组成的具有层次关系的集合。下面是它的特点&#xff1a; 根结点是没有前驱的结点&#xff08;没有父结点的结点&#xff09;子结点之间互不相交除了根结点外&#xff0c;其它结点都只有一个父结点n个结…

学习threejs,导入AWD格式的模型

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.AWDLoader AWD模型加…

Chapter4.3:Implementing a feed forward network with GELU activations

4 Implementing a GPT model from Scratch To Generate Text 4.3 Implementing a feed forward network with GELU activations 本节即将实现子模块&#xff0c;用于transformer block&#xff08;变换器块&#xff09;的一部分。为此&#xff0c;我们需要从激活函数开始。 深…