[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.mfbz.cn/a/171859.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

同为科技(TOWE)工业连接器:保障高效、可靠、安全的电气连接

国内经济快速的发展&#xff0c;人们生活水平的不断提高&#xff0c;基础设施的建设是发展的基础&#xff0c;完善的基础设施对加速经济的发展起到至关重要的作用。其中&#xff0c;基础建设中机场、港口、电力、通讯等公共设施必须配套相应的电气设施&#xff0c;工业用插头插…

js ::after简单实战

::after的作用是在元素后面再加个XXX样式 工作中遇到了一个表格&#xff0c;鼠标指到单元格要有个整行编辑态的效果&#xff0c;下面写个简单的demo 有人可能会说了&#xff0c;直接修改某个单元格的hover样式不就行了嘛&#xff0c;问题是如果鼠标指到单元格和单元格直接的…

单链表的实现(Single Linked List)---直接拿下!

单链表的实现&#xff08;Single Linked List&#xff09;—直接拿下&#xff01; 文章目录 单链表的实现&#xff08;Single Linked List&#xff09;---直接拿下&#xff01;一、单链表的模型二、代码实现&#xff0c;接口函数实现①初始化②打印链表③创建一个结点④尾插⑤尾…

Threejs_09 gltf模型的引入(效果初现)

本节使用到的图片、素材、gltf文件&#xff0c;都是老陈打码的原素材 支持&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#x…

使用USB转JTAG芯片CH347在Vivado下调试

简介 高速USB转接芯片CH347是一款集成480Mbps高速USB接口、JTAG接口、SPI接口、I2C接口、异步UART串口、GPIO接口等多种硬件接口的转换芯片。 通过XVC协议&#xff0c;将CH347应用于Vivado下&#xff0c;简单尝试可以成功&#xff0c;源码如下&#xff0c;希望可以一起共建&a…

Vue3 customRef自定义ref 实现防抖

防抖就是防止在input 框中每输入一个字符就要向服务器请求一次&#xff0c;只要在用户输入完成过一段时间再读取用户输入的内容就能解决这个问题&#xff0c;减小服务器的压力。 1. 自定义ref是一个函数&#xff0c;可以接受参数。 比如我们自定义一个myRef&#xff1a; setu…

拖拽场景遇到 iframe 无法拖拽的问题解决方案

描述一个场景&#xff1a;在网页中&#xff0c;分为上下两部分布局&#xff0c;下半部分显示操作日志&#xff0c;下半部分的区域高度是可拖拽调整的&#xff0c;但是如果下半部分嵌入一个 iframe 的时候&#xff0c;往上拖拽可以&#xff0c;但是往下拖拽&#xff0c;一旦到了…

【AICFD案例教程】IGBT对流换热分析

AICFD是由天洑软件自主研发的通用智能热流体仿真软件&#xff0c;用于高效解决能源动力、船舶海洋、电子设备和车辆运载等领域复杂的流动和传热问题。软件涵盖了从建模、仿真到结果处理完整仿真分析流程&#xff0c;帮助工业企业建立设计、仿真和优化相结合的一体化流程&#x…

技术分享| anyRTC之RTN网络

RTN(Real-time Network)中文名&#xff1a;实时音视频传输网络。 RTN是最近几年由各大RTC的云厂商提出的一个全新架构的音视频实时传输网络概念。类似于直播的CDN网络&#xff0c;RTN是对音视频的实时性又强烈要求的场景而设计的&#xff0c;原理上全球端到端的时延通过RTN网络…

springboot集成nacos作配置中心,动态配置不生效

总体概要 springboot3.0&#xff0c;nacos&#xff0c;jdk17使用nacos配置中心&#xff0c;热更新&#xff0c;使配置动态生效 本文主要介绍springboot怎么集成nacos作为配置中心&#xff0c;使其配置在不重启服务的情况下&#xff0c;怎么生效的。 所用组件及其版本 组件版本…

GNSS技术在农业领域的创新应用

全球导航卫星系统&#xff08;GNSS&#xff09;技术在农业领域的广泛应用为现代农业带来了革命性的变革。从精准农业到农业机械自动化&#xff0c;GNSS技术为提高农业生产效率、减少资源浪费、实现可持续发展提供了关键支持。本文将深入探讨GNSS技术在农业领域的应用&#xff0…

数据结构~~~~ [队列] ~~~~

文章目录 队列队列的概念与结构队列的接口实现***队列的初始化******队列的销毁******队列的插入与创建节点******队列的删除******队列的队头数据******队列的队尾数据******队列的判空*** 队列 队列的概念与结构 队列的插入数据在队尾出数据在队头&#xff08;尾入头出&…

影视行业如何远程完整快速传输大文件?

影视行业是一个充满创意和协作的领域。在影视制作中&#xff0c;涉及到多个环节和部门&#xff0c;包括编剧、导演、摄影、剪辑、配音、视效等。这些环节和部门通常分布在不同的地点&#xff0c;甚至不同的国家。因此&#xff0c;影视制作过程中需要频繁进行远程传输&#xff0…

第1关:图的邻接表存储及求邻接点操作

任务要求参考答案评论2 任务描述相关知识编程要求测试说明 任务描述 本关任务&#xff1a;要求从文件输入顶点和边数据&#xff0c;包括顶点信息、边、权值等&#xff0c;编写程序实现以下功能。 1&#xff09;构造图G的邻接表和顶点集&#xff0c;即图的存储结构为邻接表。 …

VCP-DCV VMware vSphere,即将开课~想了解点击查看

VCP-DCV VMware vSphere 本周开课~ 想报名的必须提前预约啦 &#x1f447;&#x1f447;&#x1f447; 课程介绍 本课程重点讲授如何安装、配置和管理VMware vSphere 8.0&#xff08;包括VMware ESXi™ 8.0和VMware vCenter Server™ 8.0&#xff09; 本课程将帮助您做好…

docker打包chatpdf(自写)

docker打包上传 docker build -t kitelff/chatpdf:v0.1 .##修改镜像名字 docker tag c2c1a0eb4e08 kitelff/chatpdf:v0.1## push docker push kitelff/chatpdf:v0.1上传文件&#xff0c;测试效果

Rust语言精讲:数据类型全解析

大家好&#xff01;我是lincyang。 今天&#xff0c;我们将深入探讨Rust语言中的数据类型&#xff0c;这是理解和掌握Rust的基础。 Rust语言数据类型概览 Rust是静态类型语言&#xff0c;所有变量类型在编译时确定。Rust的数据类型分为两类&#xff1a;标量类型和复合类型。…

GNSS技术在建筑和城市规划中的关键角色

全球导航卫星系统&#xff08;GNSS&#xff09;技术在当今建筑和城市规划领域扮演着至关重要的角色。这一先进的技术为城市的可持续发展、建筑工程的精准施工以及城市规划的科学决策提供了强大的支持。本文将深入探讨GNSS在建筑和城市规划中的关键角色&#xff0c;并分析其对这…

ML-Net:通过深度学习彻底改变多标签分类

一、说明 多标签分类是一项具有挑战性的机器学习任务&#xff0c;其中输入可以同时属于多个类。传统的多标签分类方法通常依赖于将问题转化为一系列二元分类任务或使用集成方法。然而&#xff0c;深度学习的出现开创了多标签分类的新时代&#xff0c;ML-Net 等模型突破了该领域…
最新文章