[C++11]可变参数模板

导览:

  1. 本章将从可变参数模板的概念开始讲起,到其究竟是如何做到实例化的
  2. 再从实例出发,探究该如何编写可变参数模板
  3. 最后涉及可变参数模板的运用
    在这里插入图片描述

什么是可变参数模板


让我们先见一下可变参数模板

template<typename ...Args>
void test(Args... args)
{//...
}

概念:

一个可变参数模板(variadic template)就是一个接受可变数目参数的模板函数或模板类。可变数目的参数称为参数包(parameter packet)。存在两种参数包:模板参数包(template parameter packet),表示零个或多个模板参数;函数参数包(function parameter packet),表示零个或多个函数参数。

几个问题:

让我们带着以下几个问题去学习可变参数模板

  1. 可变参数模板如何实例化
  2. 如何书写可变参数模板
  3. 可变参数模板的运用—emplace系列
  4. 可变参数模板的运用—包扩展
  5. 可变参数模板的运用—转发参数包

可变参数模板如何实例化


上代码:

template<typename T, typename ...Args>
void func(const T& t)
{cout << t << endl << endl;
}
template<typename T, typename ...Args>
void func(const T& t, const Args&... args)
{cout << t << endl;func(args...);
}
int main()
{func(1,2,'c',4);func(1,2,'c');func(1,2);func(1);return 0;
}

代码思路:

这里书写了一个名为func()的函数,其形参中包含了一个函数参数包args。在main函数中,分别以不同的参数数目调用了func(),然后打印结果如下

1
2
c
41
2
c1
21

解释:

这里拿func(1,2,'c',4)来说明。

先明确一点,这些实例化需要在编译阶段进行,到了运行阶段就挨个去调用参数符合的函数就行了。

可以这样理解:

当我func传参时,我的接收方是func(const T& t, const Args&... args),我传递的是一个参数包,但其实这个包就是一连串的参数(args… -> {int,int,char,int}),接收时将我传递包中的第一个元素给给t,后面的元素又形成了一个新的参数包,然后一直递归去调用实例化,最后调用到只剩一个参数时就会去调用我们写的func(),不再实例化生成新的func()(我们特意增加了单参数的func(),此时编译器就不会自己实例化生成新的,而这个func()里面不会再递归实例化任何别的版本)。

func这个函数调用总共实例化生成了4个不同版本的函数,他们分别是:

void func(const int&,const int&,const char&,const int&);
void func(const int&,const char&,const int&);
void func(const char&,const int&);
void func(const int&);

如下是运行中的调用逻辑:

调用targs…包中参数个数向下传参
func(1,2,‘c’,4)12,‘c’,43个参数的包2,‘c’,4
func(2,‘c’,4)2‘c’,42个参数的包‘c’,4
func(‘c’,4)‘c’41个参数的包4
func(4)4null空包无传参

解决几个小问题:

先引入一个运算符:sizeof...

这个运算符可以计算包中含有多少个元素,其返回值是一个常量表达式。具体代码如下:sizeof…(args)

  1. 为什么实例化递归时不能用ifsizeof...判断参数包中的元素是否为空,进而跳出循环。

    答:因为if是运行时判断,而实例化是在编译阶段进行的,所以不能用if判断

  2. 既然args…是一个参数包,那么我能不能用args[i]的方式打印这个包中的某个参数

    答:不能。同样,[]是运行时解析,而编译完后args…就不再是一个包,而是一堆参数了,自然也就不可以下标访问

如何书写可变参数模板


书写模板参数一直都是一件很苦恼的事 ,因为这个...总是不知道放在何处

建议在写的时候多试试几种方式,哪种不报错就用哪种。以下给出几点建议,以供参考:

  1. ...总是跟在参数名或类型名后面
  2. 如果想要声明模板参数包,则跟在类型名后面
  3. 如果想要展开函数参数包,则跟在参数名后面
  4. 还有某些特殊情况,多换换位置也就过了

例子:

template<typename T, typename... Args>  //声明模板参数包
void fooHelper(T t, Args... args) {  //声明模板参数包// 处理t  fooHelper(args...); // 递归调用,展开剩余参数  
}  
template<typename T>  
void fooHelper(T t) {  // 终止递归的情况  
}  
template<typename... Args>  
void foo(Args... args) {  fooHelper(args...); // 初始调用,展开所有参数  
}

可变参数模板的运用


emplace系列

这是C++11后STL库中新增的函数

template <class... Args>void emplace_back (Args&&... args);
template <class... Args>
iterator emplace (const_iterator position, Args&&... args);template <class... Args>void emplace_back (Args&&... args);
template <class... Args>iterator emplace (const_iterator position, Args&&... args);
//...

STL库中对此的解释是:Construct and insert element,构造同时插入元素

这里涉及了右值引用(但不是重点),如果想了解右值引用可以去看我的另一篇文章[C++11]右值引用

我们给出一个最简单的例子来说明:

class Date
{
public:Date(int a = 1, int b = 1, int c = 1):_a(a), _b(b), _c(c){cout << "Date()" << endl;}Date(const Date& d){cout << "const Date&" << endl;}Date(Date&& d){cout << "const Date&&" << endl;}
private:int _a;int _b;int _c;
};
int main()
{list<Date> lt1;list< pair<Date, Date>> lt2;Date d1(10,10,10);lt1.push_back(d1);lt1.push_back(move(d1));cout << "==================================" << endl;lt1.emplace_back(d1);lt1.emplace_back(move(d1));cout << "==================================" << endl;lt1.push_back({10,10,10}); cout << endl;lt1.emplace_back(10,10,10);cout << "==================================" << endl;pair<Date, Date> pr({1,1,1},{1,1,1});lt2.push_back(pr);cout << endl;lt2.push_back(move(pr));cout << "==================================" << endl;lt2.emplace_back(pr);cout << endl;lt2.emplace_back(move(pr));cout << "==================================" << endl;lt2.push_back({ (1, 1, 1), (1, 1, 1) });cout << endl;lt2.emplace_back((1, 1, 1), (1, 1, 1));return 0;
}

命令行打印结果:

Date() --构造
const Date&  --拷贝构造
const Date&& --移动构造
==================================
const Date&  --拷贝构造
const Date&& --移动构造
==================================
Date() --构造
const Date&& --移动构造Date() --构造
==================================
Date() --构造
Date() --构造
const Date&  --拷贝构造
const Date&  --拷贝构造
const Date&  --拷贝构造
const Date&  --拷贝构造const Date&& --移动构造
const Date&& --移动构造
==================================
const Date&  --拷贝构造
const Date&  --拷贝构造const Date&& --移动构造
const Date&& --移动构造
==================================
Date() --构造
Date() --构造
const Date&& --移动构造
const Date&& --移动构造Date() --构造
Date() --构造
插入方式vector lt1vector<pair<Date,Date>> lt1
push_back(d1),push左值const Date& --拷贝构造const Date& --拷贝构造
const Date& --拷贝构造
emplace_back(d1),emplace左值const Date& --拷贝构造const Date& --拷贝构造
const Date& --拷贝构造
push_back(move(d1)),push右值const Date&& --移动构造const Date&& --移动构造
const Date&& --移动构造
emplace_back(move(d1)),emplace右值const Date&& --移动构造const Date&& --移动构造
const Date&& --移动构造
push_back({10,10,10}),push用initial_listDate() --构造
const Date&& --移动构造
null
emplace_back(10,10,10),emplace用可变模板参数Date() --构造null
push_back({(10,10,10),(10,10,10)}),push用initial_listnullDate() --构造
Date() --构造
const Date&& --移动构造
const Date&& --移动构造
emplace_back((1, 1, 1), (1, 1, 1)),emplace用可变模板参数nullDate() --构造
Date() --构造

如果有朋友下定决心也尝试一下,结果可能会发现,哎?…怎么我的push和emplace一点规律都没有???甚至会发生错误!

这很有可能是你用的不是list,而是vector之类的顺序容器,这就不得不谈到vector的扩容问题了,对!这都是由于扩容搞的鬼,扩容导致了资源的重新分配。请记住:因为扩容问题导致vector和list的push、emplace的底层实现是很不同的。

是不是看起来很复杂,其实一点也不简单!但是我们只需要记住如下几点,就能明白emplace是用来干嘛的了

  1. 记住!emplace的作用就是利用可变模板参数优化效率

  2. 对于类创建的对象,无论是用push还是用emplace他们的结果都一样(例如上表前4个)。这是由于push里面也实现了右值引用版本(很显然,他们的效率只和左值、右值引用相关),并且用对象初始化与今天所讲的参数包没有任何关系。

  3. 最后4个,两两一组相比较,很明显能发现push和emplace版本相差甚大,这就是我们的可变参数模板优化的成果!其原理也很简单:

    emplace中用接受的参数包直接去构造Date

    而push版本中则需要在进push前先构造Date,再将参数传入

    但其实也还好,毕竟push都实现了右值引用版本,传入参数的时候会调用移动构造,开销也不会太大。

包扩展

  • 包扩展(Pack Expansion)是C++编程中的一个概念,它主要应用在模板元编程中,特别是可变参数模板函数中。包扩展允许程序员将参数包(parameter pack)展开为一系列独立的参数,以便在函数模板或类模板中使用。
  • 对于一个参数包,除了获取其大小外,我们能对他做的唯一的事情就是扩展(expand)它。当扩展一个包时,我们还要提供用于每个扩展元素的模式

大家回顾一下前面的内容,其实我们一开始实例化打印的例子,就是一种包扩展

举一个比实例化打印更复杂的例子:

//如果你需要对某些信息进行处理(如错误信息),我们可以实现出将这个包中的每一个参数都当作实参传进一个函数
template <typename... Args>
ostream& errorMsg(ostream &os,const Args&...rest)
{return print(os,debug(rest)...);//debug是一个函数
}
//上述print就好像我们写如下代码
print(os,debug(x1),debug(x2),debug(x2)...);//这里的省略号就是省略号

转发参数包

这里就简要说明一下

转发参数包(Forwarding Parameter Pack)是C++模板编程中的一个高级特性,它允许你将参数包原封不动地传递给其他函数或模板,同时保持参数的原始类型和值类别(左值或右值)。这在编写通用包装器(wrappers)或代理(delegates)时特别有用,因为你可以确保参数在传递过程中不会被不必要地拷贝或移动。像前面的emplace就是使用该技术。

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

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

相关文章

【SpringCloud】一文详谈Nacos

&#x1f3e1;浩泽学编程&#xff1a;个人主页 &#x1f525; 推荐专栏&#xff1a;《深入浅出SpringBoot》《java对AI的调用开发》 《RabbitMQ》《Spring》《SpringMVC》《项目实战》 &#x1f6f8;学无止境&#xff0c;不骄不躁&#xff0c;知行合一 文章目录 …

Linux之用户账号、用户组和与账号有关的系统文件

目录 一、基本介绍 1.用户和用户组 2.UID和GID 二、 账户管理 1.查看用户的UID和GID 2.添加账户 3.删除账号 4.修改账号 5.账户口令 三、分组管理 1.新增用户组 2.删除用户组 3.修改用户组 4.用户组切换 四、与账号有关的系统文件 1./etc/passwd 2./etc/shado…

linux进程一篇全解

1、查看进程 ps axuf ---静态查看所有进程#user 用户#PID 每个进程的标识符&#xff0c;父进程为1#VSZ 虚拟内存#RSS 实际内存#pts 窗口 TTY系统启动窗口# %MEM 内存#STAT 该进程的状态&#xff0c;包括&#xff1a;S 可中断睡眠Ss 父进程S< 优先级较高SN…

李宏毅深度强化学习导论——当奖励是稀疏的

引言 这是李宏毅强化学习的笔记&#xff0c;主要介绍如何处理稀疏奖励问题。 稀疏奖励 当我们拿Actor和环境互动后可以得到很多奖励&#xff0c;整理之后可以得到分数 A A A&#xff0c;然后可以训练Actor。 但RL中有时会出现多数情况下奖励为零&#xff0c;此时我们不知道动…

行存储与列存储:大数据存储方案的选择与优缺点分析

随着大数据时代的来临&#xff0c;数据的规模和复杂性呈指数级增长&#xff0c;传统的关系数据库已经不再适应这一巨大的存储量和计算要求。在大数据存储领域&#xff0c;行存储和列存储成为两种备受关注的存储方案。本文将探讨行存储和列存储的定义、优缺点&#xff0c;并结合…

第十四届省赛大学B组(C/C++)岛屿个数

目录 题目链接&#xff1a;岛屿个数 解题思路&#xff1a; AC代码&#xff08;BFSDFS&#xff09;&#xff1a; 题目链接&#xff1a;岛屿个数 小蓝得到了一副大小为 MN 的格子地图&#xff0c;可以将其视作一个只包含字符 0&#xff08;代表海水&#xff09;和 1&#xff0…

LeetCode-331. 验证二叉树的前序序列化【栈 树 字符串 二叉树】

LeetCode-331. 验证二叉树的前序序列化【栈 树 字符串 二叉树】 题目描述&#xff1a;解题思路一&#xff1a;看提示主要是栈和树。这题其实不是二叉树的遍历题&#xff0c;而是检验二叉树基础知识的题&#xff0c;也许有些难想。第一种解法是&#xff1a;把有效的叶子节点使用…

【DETR系列目标检测算法代码精讲】01 DETR算法03 Dataloader代码精讲

与一般的Dataloader的区别在于我们对图像进行了随机裁剪&#xff0c;需要进行额外的操作才能将其打包到dataloader里面 这一段的代码如下&#xff1a; if args.distributed:sampler_train DistributedSampler(dataset_train)sampler_val DistributedSampler(dataset_val, shu…

Python 自学(九) 之异常处理,文件及目录操作

目录 1. try ... except ... else ... finally 排列 P231 2. write, read, seek, readline, readlines 基本文件操作 P245 3. os模块 基本目录操作 P249 4. os.path 模块 复杂目录操作 P250 5. os 模块 高…

js 双冒号运算符(::)

双冒号运算符::是 ES7 中提出的函数绑定运算符&#xff0c;用来取代call()、apply()、bind()调用。 双冒号左边是一个对象&#xff0c;右边是一个函数。该运算符会自动将左边的对象&#xff0c;作为上下文环境&#xff08;即this对象&#xff09;&#xff0c;绑定到右边的函数…

超越35岁的编码之路:资深程序员的挑战与机遇

程序员35岁会失业吗&#xff1f; 35岁被认为是程序员职业生涯的分水岭&#xff0c;许多程序员开始担忧自己的职业发展是否会受到年龄的限制。有人担心随着年龄的增长&#xff0c;技术更新换代的速度会使得资深程序员难以跟上&#xff1b;而另一些人则认为&#xff0c;丰富的经…

Spring之循环依赖

什么是循环依赖? 依赖的相互引用,如下列的这种形式 Component public class A {Autowiredprivate B b;}Component public class B {Autowiredprivate A a; } Spring是如何解决循环依赖的 Spring是通过三级缓存来解决循环依赖 singletonObjects : 单例bean,已经实例化,完成…

牛客2024年愚人节比赛(A-K)

比赛链接 毕竟是娱乐场&#xff0c;放平心态打吧。。。 只有A一个考了数学期望&#xff0c;其他的基本都是acmer特有的脑筋急转弯&#xff0c;看个乐呵即可。 A 我是欧皇&#xff0c;赚到盆满钵满&#xff01; 思路&#xff1a; 我们有 p 1 p_1 p1​ 的概率直接拿到一件实…

CMake常用示例

常用示例 入门 Hello CMake CMake 是一个用于配置跨平台源代码项目应该如何配置的工具建立在给定的平台上。 ├── CMakeLists.txt # 希望运行的 CMake命令 ├── main.cpp # 带有main 的源文件 ├── include # 头文件目录 │ └── header.h └── src # 源代码目录 ├…

【代码随想录】day32

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、122买卖股票的最佳时机II二、55跳跃游戏三、45跳跃游戏II 一、122买卖股票的最佳时机II 方法1:计算斜率大于0的线段的diffY class Solution { public:int max…

Redis改造原始代码

基础篇Redis 5.2.2.改造原始代码 代码说明: 1.在我们完成了使用工厂设计模式来完成代码的编写之后&#xff0c;我们在获得连接时&#xff0c;就可以通过工厂来获得。 &#xff0c;而不用直接去new对象&#xff0c;降低耦合&#xff0c;并且使用的还是连接池对象。 2.当我们…

FreeROST作业day2

1.总结串口的发送和接收功能使用到的函数 串口发送数据函数&#xff1a; HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout); UART_Handle…

C语言经典例题(16) --- 按照格式输入并交换输出、计算机表达式的值、能活多少秒、喝酸奶、竞选社长

1.按照格式输入并交换输出 题目描述: 输入两个整数&#xff0c;范围-231~231-1&#xff0c;交换两个数并输出。 输入描述: 输入只有一行&#xff0c;按照格式输入两个整数&#xff0c;范围&#xff0c;中间用“,”分隔。 输出描述: 把两个整数按格式输出&#xff0c;中间用“…

【LeetCode】热题100:排序链表

题目&#xff1a; 给你链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排序后的链表 。 示例 1&#xff1a; 输入&#xff1a;head [4,2,1,3] 输出&#xff1a;[1,2,3,4] 示例 2&#xff1a; 输入&#xff1a;head [-1,5,3,4,0] 输出&#xff1a;[-1,0,3,4,5] …

linux进程fork函数的讲解。

通过指令,查看接口的详细信息 man forkOn success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately. 这里的返回值的意…