[C++11]可变参数模板和参数包展开

可变参数模板

文章目录

    • 可变参数模板
      • 可变参数模板的概念
      • 可变参数模板的定义方式
    • 模板参数包的展开
      • 递归展开参数包
        • sizeof...计算参数包大小
      • 逗号表达式展开参数包
      • enable_if方式展开
      • 折叠表达式展开
    • 总结

可变参数模板的概念

可变参数模板(Variadic templates)C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。

  • C++11之前,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。
  • 在C++11之前其实也有可变参数的概念,比如printf函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。

可变参数模板的定义方式

template <class... T>
void func(T... args)
{//...
}

上面的我们把带…的模板参数称为模板参数包(template parameter pack)

上面这个函数模板的参数 args 前面有省略号,我们称之为模板参数包(template parameter pack)的可变模版参数,它里面包含了0到N个模版参数,而我们是无法直接获取 args 中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数。

模板参数包的展开

递归展开参数包

递归展开的方式如下:

  • 函数模板除去参数包可变模板参数外至少要有一个模板参数,用于从参数包中拿出一个参数
  • 剩余参数包递归调用
  • 每次剥离一个参数,直至参数包空
template <class T, class... Args>
void foo(T first, Args... args)
{cout << first << " ";foo(args...);
}
//int main()
//foo("good", 2, "hello", 4, 110);

我们写出如上一个带可变模板参数的函数foo,我们尝试在main函数中调用发现无法调用

有递归自然有出口,当函数包空时,仍会递归一个空包作为参数,而我们没有空参数函数,所以我们还要再增加一个空参数的函数来进行特化

如下:

void foo()
{cout << endl;
}template <class T, class... Args>
void foo(T first, Args... args)
{cout << first << " ";foo(args...);
}int main()
{foo(1, 2, 3, 4);foo("good", 2, "hello", 4, 110);return 0;
}
//输出
//1 2 3 4 
//good 2 hello 4 110

当然我们可以规定递归出口为其他数量的参数,如:

void foo(int a)
{cout << endl;
}template <class T, class... Args>
void foo(T first, Args... args)
{cout << first << " ";foo(args...);
}int main()
{foo(1, 2, 3, 4);foo("good", 2, "hello", 4, 110);return 0;
}
//输出
//1 2 3 
//good 2 hello 4

当然,当我们定义递归出口为一个参数的函数时,我们调用foo必须传入不少于一个参数

sizeof…计算参数包大小

我们其实是可以计算参数包的大小的,如sizeof…(args)

void foo()
{cout << endl;
}
template <class T, class... Args>
void foo(T first, Args... args)
{cout << first << " " << sizeof...(args) << endl;foo(args...);
}int main()
{foo(1, 2, 3, 4);return 0;
}
//输出
//1 3
//2 2
//3 1
//4 0

那么我们是否可以通过对参数包大小的判断来结束函数递归,从而省去无参数或者少参数函数作为递归出口呢?

template <class T, class... Args>
void foo(T first, Args... args)
{cout << first << " " << sizeof...(args) << endl;if (!sizeof...(args))return;foo(args...);
}

我们发现直接报错了,也就是说这种方式不可行

  • 我们在学习函数模板时知道,函数模板并不能直接调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数,这个生成的函数才能够被调用。
  • 而这个推演过程是在编译时进行的,我们函数体中递归调用函数,也就是说推演会不断继续下去,仍会进行参数包为空的函数推演,此时就会报错了,因为我们的函数至少要有一个参数,而又没有重载的空参数函数了
  • 这里的if判断是在代码编译结束后,运行代码时才会所走的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑。

逗号表达式展开参数包

逗号表达式展开包其实是利用了C++11新特性,列表初始化。

我们列表初始化的原理就是先用列表构建initializer_list,再用initializer_list去构建我们的容器

如果我们把参数包放入初始化列表中会怎样呢?

template <class... Args>
void foo(Args... args)
{initializer_list<int> a{args...};for (auto x : a)cout << x << " ";
}
//输出
//1 2 3 4 

我们发现参数包放入初始化列表中,由于初始化列表从左往右执行,参数包中的参数会被逐个取出,此时由于没有递归展开,所以我们不需要再额外定义空参数的重载函数,传入参数也没有数目限制。

利用初始化列表和逗号表达式结合,我们可以如下展开参数包:

template <class... Args>
void foo(Args... args)
{(void)initializer_list<int>{(cout << args << " ", 0)...};
}int main()
{foo(1, 2, 3, 4, "GenshinImpact", 3.14);return 0;
}
//输出
//1 2 3 4 GenshinImpact 3.14

我们发现很顺利的输出了,甚至不受类型限制

其实剖析一下发现逗号表达式是一种很犯规的写法,我们逗号表达式的返回值是最右边的表达式,也就是0,所以最终用来初始化列表的元素是0,但是由于列表初始化要从左向右执行,所以我们的参数包会被展开,假如参数包有N个参数,我们展开N次,但是此次返回值都是0,所以得到了N个0的列表,而参数包内的内容都被输出了。

enable_if方式展开

enable_if是C++11新引入的一个结构体,定义如下:

  // Primary template./// Define a member typedef @c type only if a boolean constant is true.template<bool, typename _Tp = void>struct enable_if{ };// Partial specialization for true.template<typename _Tp>struct enable_if<true, _Tp>{ typedef _Tp type; };

我们可以看出下面是上面的一个偏特化。当我们传入第一个参数为true时会用第二个模板来实例化,将_Tp typedef为type,而第一个模板什么也没做。

故而enable_if常用于需要根据不同的类型的条件实例化不同模板的情形。也就是说,在不同条件下选用不同类型,其广泛的应用在 C++ 的模板元编程(meta programming)之中,利用的就是SFINAE原则,英文全称为Substitution failure is not an error,意思就是匹配失败不是错误,假如有一个特化会导致编译时错误,只要还有别的选择,那么就无视这个特化错误而去选择另外的实现。

因而我们可以借此来解决我们递归展开函数包递归出口函数和参数限制的问题。

具体流程就是:

  • 利用参数包构建tuple(元组)
  • 以下标访问元组元素,同时利用下标是否等于元组元素个数作为条件重载两个函数
  • 当下标小于value,那么对对应下标元素操作
  • 当下标等于value,则进入对应函数体

代码如下:

template <size_t k = 0, class tup>
typename enable_if<k == std::tuple_size<tup>::value>::type _foo(const tup &t)
{cout << endl;
}
template <size_t k = 0, class tup>typename enable_if < k<std::tuple_size<tup>::value>::type _foo(const tup &t)
{cout << get<k>(t) << " ";_foo<k + 1, tup>(t);
}template <class... Args>
void foo(Args... args)
{_foo<0>(make_tuple(args...));
}int main()
{foo(2023, "GenshinImpact", "hello", 2024);return 0;
}

优雅,实在是太优雅了。

折叠表达式展开

前面几种都是C++11的内容,而我们的折叠表达式(Fold Expressions)则是我们C++17的新语法特性,使用折叠表达式可以简化对C++11中引入的参数包的处理,可以在某些情况下避免使用递归,更加方便的展开参数。

如下示例:

template <class... Args>
void foo(Args... args)
{(cout << ... << args) << endl;
}int main()
{foo(2023, "GenShinImpact", "hello");return 0;
}

简洁了不少,但是如何格式化呢?需要增加格式化辅助函数。

template <class T>
string format(const T &t)
{stringstream ss;ss << " " << t << " ";return ss.str();
}template <class... Args>
void foo(Args... args)
{(cout << ... << format(args)) << endl;
}int main()
{foo(2023, "GenShinImpact", "hello");return 0;
}
//输出
// 2023  GenShinImpact  hello 

也可以直接利用逗号表达式进行简化

template <class... Args>
void foo(Args... args)
{(cout << ... << (cout << args, " ")) << endl;
}int main()
{foo(2023, "GenShinImpact", "hello");return 0;
}
//输出
//2023 GenShinImpact hello 

括号里逗号表达式的返回值是" “,当输出完从参数包里拆出的args,返回” "给左边的输出流输出

总结

可变参数模板参数高度泛化,提高了编程的泛用性。

而为了实现可变参数模板我们引入了参数包,于是需要对参数包进行展开,我们可以:

  1. 通过递归每次拆出一个参数,展开参数包
  2. 利用初始化列表展开参数包
  3. 通过enable_if和元组结合展开参数包
  4. C++17直接引入折叠表达式,便于展开参数包

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

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

相关文章

Excel数据可视化—波士顿矩阵图【四象限图】

EXCEL系列文章目录 Excel系列文章是本人亲身经历职场之后萌发的想法&#xff0c;为什么Excel覆盖如此之广&#xff0c;几乎每个公司、学校、家庭都在使用&#xff0c;但是它深藏的宝藏功能却很少被人使用&#xff0c;PQ、BI这些功能同样适用于数据分析&#xff1b;并且在一些需…

Revive开发商加入VR开源标准OpenXR

作为一款能让HTC Vive用户玩到Oculus平台游戏的软件&#xff0c;它的开发商CrossVR今日宣布即将加盟为VR和AR应用程序开源组织&#xff0c;即OpenXR。 由Khronos Group引领的OpenXR旨在创建一个标准化且免版税的应用程序编程接口&#xff08;API&#xff09;&#xff0c;该API…

计算机毕业设计选题推荐-内蒙古旅游微信小程序/安卓APP-项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

代码随想录第六十三天 | 单调栈:寻找 左边 / 右边 距离当前元素最近的 更小 元素的 下标(暴力,双指针,单调栈)(84);代码随想录主要题目结束

1、寻找 左边 / 右边 距离当前元素最近的 更小 元素的 下标 1.1 leetcode 84&#xff1a;柱状图中最大的矩形 第一遍代码思路错了&#xff0c;如&#xff1a;输入[2,1,2]&#xff0c;对于2&#xff0c;因为比栈顶元素1大&#xff0c;然后就会直接得出2&#xff08;1&#xff…

PaaS、 IaaS 和 SaaS 的区别

我感觉我有点捂了 iaas&#xff0c;paas&#xff0c;和saas的区别&#xff0c;以及他们啥意思了 简单说就是&#xff0c;一个公司有很多项目&#xff0c;要管理这些项目&#xff0c;每个项目都有很多组成部分需要管理的地方&#xff0c;例如&#xff0c;存储代码&#xff0c;例…

HTML5学习系列之项目实战1

HTML5学习系列之项目实战1 前言代码记录问题总结 前言 学习记录 代码 <div id"player"><audio id"musicbox"></audio><div id"controls" class"clearfix controls"><div id"play" class"…

uniapp中实现圆形进度条的方式有哪些?

前言 在uniapp开发小程序或者apk时&#xff0c;页面需要用到一个圆形进度条&#xff08;带文字和百分比的&#xff09;&#xff0c;自己也自定义过一个,但是有一点小问题&#xff0c;咱先展示如何引入插件市场的在介绍自定义的&#xff01;一共四种&#xff0c;但是你需要考虑自…

高压放大器使用方法介绍

高压放大器是一种用于放大高压信号的电子设备&#xff0c;常用于科学研究、工业应用和医疗设备等领域。它可以将低电压信号放大到较高的电压水平&#xff0c;以满足特定应用的需求。 使用高压放大器需要注意以下几个方面&#xff1a; 1.了解设备规格&#xff1a;在使用高压放大…

0时区格林威治时间转换手机当地时间-Android

假设传入的是2023-11-01T12:59:10.420987这样的格式 要将格式为2023-11-01T12:59:10.420987的UTC时间字符串转换为Android设备本地时间&#xff0c;您可以使用java.time包中的类&#xff08;在API 26及以上版本中可用&#xff09;。如果您的应用需要支持较低版本的Android&…

【外汇天眼】交易之路:从无知到觉醒,揭秘成功交易员的五个成长阶段

世界顶尖交易员的成功背后隐藏的真正秘诀引人瞩目。许多人梦想着像电影中的主角一样&#xff0c;成为一名成功的金融交易员。尽管开设交易账户相对简单&#xff0c;但要达到稳定盈利的境界确实非常不容易。众所周知&#xff0c;在衍生品市场中&#xff0c;有80%甚至90%以上的交…

网络安全涉及哪些方面?

1.系统安全&#xff1a;运行系统安全即保证信息处理和传输系统的安全。它侧重于保证系统正常运行&#xff0c;避免因为系统的损坏而对系统存储、处理和传输的消息造成破坏和损失&#xff0c;避免由于电磁泄露&#xff0c;产生信息泄露&#xff0c;干扰他人或受他人干扰。 2. 网…

docker部署jdk21的镜像

docker Docker是一种开放源代码软件&#xff0c;可以帮助开发人员更轻松地创建、部署和运行应用程序。它是一种容器化技术&#xff0c;可以将应用程序及其依赖项打包在一个容器中&#xff0c;从而使应用程序更加便携和可移植。Docker将操作系统、应用程序和硬件虚拟化进行了彻底…

基于单片机设计的气压与海拔高度检测计(采用MPL3115A2芯片实现)

一、前言 随着科技的不断发展&#xff0c;在许多领域中&#xff0c;对气压与海拔高度的测量变得越来越重要。例如&#xff0c;对于航空和航天工业、气象预报、气候研究等领域&#xff0c;都需要高精度、可靠的气压与海拔高度检测装置。针对这一需求&#xff0c;基于单片机设计…

2023年中国聚氨酯树脂涂料需求量、市场规模及行业趋势分析[图]

聚氨酯是一种新兴的有机高分子材料&#xff0c;被誉为“第五大塑料”&#xff0c;因其卓越的性能而被广泛应用于国民经济众多领域。产品应用领域涉及轻工、化工、电子、纺织、医疗、建筑、建材、汽车、国防、航天、航空等。2022年中国聚氨酯产量已达1600万吨。 2012-2022年中国…

从傅里叶变换,到短时傅里叶变换,再到小波分析(CWT),看这一篇就够了(附MATLAB傻瓜式实现代码)

本专栏中讲了很多时频域分析的知识&#xff0c;不过似乎还没有讲过时频域分析是怎样引出的。 所以本篇将回归本源&#xff0c;讲一讲从傅里叶变换→短时傅里叶变换→小波分析的过程。 为了让大家更直观得理解算法原理和推导过程&#xff0c;这篇文章将主要使用图片案例。 一…

nginx-编译安装-基础指令-信号

nginx 的编译与安装 nginx目录介绍 如果我们需要整合第三方模块&#xff0c;需要自己编译然此模块编译到nginx里面。apt和yum的安装只具有常用的基础功能。 下载nginx wget http://nginx.org/download/nginx-1.14.0.tar.gz/auto 目录 Changes 描述了一每个版本提供了那些特…

第2关:图的深度优先遍历

任务要求参考答案评论2 任务描述相关知识编程要求测试说明 任务描述 本关任务&#xff1a;以邻接矩阵存储图&#xff0c;要求编写程序实现图的深度优先遍历。 相关知识 图的深度优先遍历类似于树的先序遍历, 是树的先序遍历的推广&#xff0c;其基本思想如下&#xff1a; …

Linux之进程概念(一)

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、冯诺依曼体系结构二、操作系统(Operator System)1、概念2、设计OS的目的3、定位4、如何理…

OceanBase:集群常见操作

目录 1.查看 OBD 管理的集群列表 2.查看某个集群状态 3.启动 OceanBase 集群 4.连接 OceanBase 集群 5.停止运行中的集群 6.销毁已部署的集群 7.查看集群配置项 8.修改集群配置项 1.查看 OBD 管理的集群列表 obd cluster list 2.查看某个集群状态 obd cluster displa…

如何利用CHATGPT写主题文章

问CHAT&#xff1a;新课标下畅言智慧课堂助力小学生量感培养&#xff0c;拟解决的关键问题 CHAT回复&#xff1a; 1. 确定智慧课堂在新课标下的正确应用方法&#xff1a;新课标对教育方法、内容等提出了新的要求&#xff0c;需要探讨如何将智慧课堂与新课标相结合&#xff0c;…