可变参数模板(C++11)

news2024/11/25 4:21:54

这篇文章讲解的是C++11的特性之一——可变参数模板,适合有一定基础的同学学习,如果是刚入门的同学可以看我过往的文章:C++基础入门

可变参数模板(Variadic Templates)是C++的一种高级特性,它允许你编写接受任意数量模板参数的模板。可变参数模板在函数、类和其他模板中都可以使用。

1. 可变参数模板的基本语法

template<typename... Args>
void func(Args... args) {
    // 在这里可以使用 args...
}
  • typename... Args:这里的 Args... 表示这是一个模板参数包,它可以包含任意数量的模板参数,类型也可以不同。
  • args...:这是一个函数参数包,对应于模板参数包 Args...。它可以包含任意数量的参数。

2. 递归展开

作用

可变参数模板的主要作用是简化处理不确定数量参数的场景。比如,你可以使用它来创建一个函数,可以接受任意数量的参数,而无需为每种参数数量情况写不同的函数重载。

在C++11之前,可变参数也仅仅限于函数参数,比如最常见的是我们的老朋友printf函数,而今天提到的是模板的可变参数。

示例

假设你要写一个打印多个参数的函数,可以这样做:

#include <iostream> 
// 基本模板:递归终止条件,无参的递归终止函数
void print() {
    std::cout << "End of recursion\n";
}

// 可变参数模板
template<typename T, typename... Args>
void print(T first, Args... args) {
    std::cout << first << std::endl;  // 打印第一个参数
    print(args...);  // 递归调用自身,继续打印剩余的参数
}

int main() {
    print(1, 2.5, "Hello", 'A');
    return 0;
}

在这个例子中,print 函数接受了任意数量的参数,并依次打印它们。递归的终止条件是函数没有参数时调用的 void print() 重载。注意参数包是不支持args[i]来获取参数的。

如果想在没有传参数的时候也是走函数模板的代码,可以改成这样:

#include <iostream>
// 可变参数模板:处理任意数量的参数,包括没有参数的情况
template<class T, class... Args>
void ShowListArg(T value, Args... args) {
    std::cout << value << " "; 
    ShowListArg(args...);  // 递归调用,继续处理剩余的参数
}

// 空参数包的情况
template<class... Args>
void ShowListArg() {
    std::cout << std::endl;  // 当没有参数时,直接打印换行符
}

// 包装函数
template<class... Args>
void ShowList(Args... args) {
    ShowListArg(args...);  // 调用处理函数
}

int main() {
    ShowList(1, 2.5, "Hello", 'A');  // 正常的参数调用
    ShowList();  // 没有参数的调用
    return 0;
}

带参的递归终止函数

如果你希望递归终止条件的函数也带有一个参数,可以通过限制参数的数量,使其只处理一个参数而不再递归调用。这就是递归的“基础条件”,在这种情况下,函数只需要处理最后一个参数。以下是一个带有参数的递归终止函数的例子:

#include <iostream>

// 递归终止函数:处理最后一个参数
template<class T>
void ShowListArg(T value) {
    std::cout << value << std::endl;  // 打印最后一个参数,并换行
}

// 可变参数模板:处理多个参数的情况
template<class T, class... Args>
void ShowListArg(T value, Args... args) {
    std::cout << value << " "; 
    ShowListArg(args...);  // 递归调用,继续处理剩余的参数
}

// 包装函数
template<class... Args>
void ShowList(Args... args) {
    ShowListArg(args...);  // 调用处理函数
}

也就是说这个函数至少要传一个参数,如果不传参数的话就会报错。

能不能把参数包放到数组里?

不能直接将不同类型的参数放入原生数组中,可以使用 std::initializer_list 或者 std::array 来处理参数包中的参数。

下面是一个示例,使用 std::initializer_list 将参数包中的参数放入数组并进行处理:

#include <iostream>
#include <initializer_list>

// 包装函数,用于将参数包转为 std::initializer_list
template<typename... Args>
void ShowList(Args... args) {
    std::initializer_list<int> list{ args... };
    for (auto value : list) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    ShowList(1, 2, 3, 4);  // 只支持同一类型的参数
    return 0;
}

限制

使用 std::initializer_list 时,所有参数必须是同一类型(如上例中的 int)。如果你希望处理不同类型的参数,则需要使用其他方法,如 std::tuple 或者变体类(例如 std::variant)。以下是一个使用 std::tuple 的示例:(这段代码有点难,但不是这篇文章的重点,可以暂时忽略,感兴趣可以借助ai来学习

#include <iostream>
#include <tuple>

// 辅助函数,用于递归地打印 std::tuple 中的元素
template<std::size_t Index = 0, typename... Args>
void printTuple(const std::tuple<Args...>& t) {
    if constexpr (Index < sizeof...(Args)) {
        std::cout << std::get<Index>(t) << " ";
        printTuple<Index + 1>(t);
    }
    else {
        std::cout << std::endl;
    }
}

// 包装函数,将参数包放入 std::tuple
template<typename... Args>
void ShowList(Args... args) {
    auto t = std::make_tuple(args...);  // 创建 std::tuple
    printTuple(t);  // 打印 tuple 中的所有元素
}

int main() {
    ShowList(1, 2.5, "Hello", 'A');  // 支持不同类型的参数
    return 0;
}
  • std::tuplestd::tuple 是一个可以包含多个不同类型元素的容器。我们将参数包 args... 放入 std::tuple 中,以便处理不同类型的参数。

  • printTuple 函数

    • 这个函数使用递归方式来打印 std::tuple 中的每个元素。
    • if constexpr 是一种在编译时进行条件判断的方式,当递归到达 tuple 的末尾时,停止递归并打印换行符。

这个示例支持将不同类型的参数放入数组并进行处理。你可以根据你的需求选择适合的方式。


2. 折叠表达式展开

使用折叠表达式来展开参数包是一种高级技巧,它可以在处理可变参数模板时简化代码。

下面是一个示例:

#include <iostream>

// 使用逗号表达式展开参数包
template<typename... Args>
void ShowList(Args... args) {
    (std::cout << ... << args) << std::endl;
}

int main() {
    ShowList(1, 2.5, "Hello", 'A');  // 调用示例
    return 0;
}
  • (std::cout << ... << args):这是一个使用折叠表达式(fold expression)的语法,它可以对参数包进行操作。
    • ...:表示参数包的展开位置。
    • std::cout << args:表示将参数包中的每个元素依次输出到 std::cout

逗号表达式与折叠表达式的结合

在更传统的情况下,逗号表达式通常与初始化列表一起使用来展开参数包:

#include <iostream>

// 使用逗号表达式和初始化列表展开参数包
template<typename... Args>
void ShowList(Args... args) {
    (void)std::initializer_list<int>{(std::cout << args << " ", 0)...};
    std::cout << std::endl;
}

int main() {
    ShowList(1, 2.5, "Hello", 'A');  // 调用示例
    return 0;
}

解释

  • std::initializer_list{(std::cout << args << " ", 0)…}
    • {(std::cout << args << " ", 0)...}:这是逗号表达式在初始化列表中的应用。这里的 std::cout << args << " " 负责输出每个参数,逗号后的 0 是为了满足 std::initializer_list 的类型要求。
    • ... 负责展开参数包 args...,将每个 args 依次传递给表达式 (std::cout << args << " ", 0),然后将结果(即 0)放入 std::initializer_list<int> 中。
    • 使用 (void) 是为了忽略 std::initializer_list 的结果,因为我们只关心输出操作。

总结

  • 使用折叠表达式 (std::cout << ... << args) 是现代 C++(C++17 及以后)的简洁做法,它直接对参数包进行展开,并将结果输出。
  • 使用传统的逗号表达式和初始化列表是一种更通用的方法,适用于更早版本的 C++,但是不是很推荐。

这两种方法都可以有效地展开参数包,并执行所需的操作。根据你的编译器支持情况,你可以选择其中一种方式来使用。


3. emplace_back

在这里插入图片描述
在C++11中,STL的容器加入了emplace系列的接口,支持模板的可变参数
在这里插入图片描述
注意:此处的"&&"表示的是万能引用,详细可见上一篇文章(链接)

在 C++ 中,std::list 中的 push_backemplace_back 是用于向列表的末尾添加元素的两个函数,但它们在使用方式和效率上有一些重要的区别。

1. push_back

  • 用法: push_back 接受一个已存在的对象或对象的副本作为参数,并将其添加到列表的末尾。

  • 过程:

    • 传入的对象首先会被复制(或者移动,如果支持移动语义)。
    • 然后,std::list 会调用该对象的拷贝构造函数(或移动构造函数),将对象放入列表中。
  • 示例:

    std::list<std::string> myList;
    std::string str = "Hello";
    myList.push_back(str);  // 传入的是对象的副本
    
  • 性能影响: 由于需要复制(或移动)对象,push_back 在某些情况下可能会有额外的性能开销,特别是在处理大型对象时。

2. emplace_back

  • 用法: emplace_back 直接在列表末尾构造一个对象。它接受构造函数的参数,然后在列表末尾调用构造函数来创建对象。

  • 过程:

    • emplace_back 不需要先创建对象并再进行复制或移动,而是直接在目标位置调用构造函数进行对象构造。
    • 它可以避免不必要的复制或移动,从而提高性能。
  • 示例:

    std::list<std::string> myList;
    myList.emplace_back("Hello");  // 直接在列表中构造对象
    
  • 性能优势: emplace_back 直接构造对象,避免了对象的拷贝或移动,这在处理复杂对象或大对象时尤其高效。

当你需要向列表中添加对象,并且该对象的构造过程较为复杂或你希望避免不必要的拷贝时,emplace_back 是更好的选择。如果你已经有一个现成的对象,并且只是需要将它添加到列表中,那么使用 push_back 也是完全可以的。

传参数包

当你使用参数包传递给 push_backemplace_back 时,两者的行为仍然有所不同,特别是当你处理参数包中的多个参数时。

1. push_back 和参数包

push_back 不能直接接受参数包,因为 push_back 只接受一个已经构造好的对象。如果你传递参数包给 push_back,首先你需要将参数包展开并用于构造对象,然后将这个构造好的对象传递给 push_back

示例:

#include <list>
#include <string>

template <typename T, typename... Args>
void addToList(std::list<T>& lst, Args&&... args) {
    lst.push_back(T(std::forward<Args>(args)...));
}

int main() {
    std::list<std::string> myList;
    addToList(myList, "Hello", 5, 'a'); // 传递给std::string的构造函数
    return 0;
}

2. emplace_back 和参数包

emplace_back 可以直接接受参数包并将其转发给对象的构造函数。因此,当你传递参数包给 emplace_back 时,它会将这些参数直接用于在容器中构造对象,而不会进行多余的拷贝或移动操作。

示例:

#include <list>
#include <string>

template <typename T, typename... Args>
void emplaceToList(std::list<T>& lst, Args&&... args) {
    lst.emplace_back(std::forward<Args>(args)...);
}

int main() {
    std::list<std::string> myList;
    emplaceToList(myList, "Hello", 5, 'a'); // 在容器中直接构造std::string
    return 0;
}

3. 区别总结

  • push_back 和参数包:

    • 你需要手动展开参数包,构造对象,然后将该对象传递给 push_back
    • 这种方式可能会导致额外的对象拷贝或移动操作。
  • emplace_back 和参数包:

    • 可以直接将参数包传递给 emplace_back,由它在容器中直接构造对象。
    • 更加高效,避免了不必要的拷贝和移动操作。

4. 实际应用

  • 当你需要在容器中直接构造对象,并且传递了多个参数时,emplace_back 是更好的选择,因为它更高效且代码更简洁。
  • 如果你需要传递已经构造好的对象,或者你不能直接使用构造函数参数,则只能使用 push_back

这样做的好处在于使用 emplace_back 时,你可以省去中间对象的创建,直接在容器中进行对象的构造,尤其是在处理参数包时,emplace_back 能够让代码更具表现力和效率。

可变参数模板非常强大,但也可能让代码变得复杂,所以在使用时需要小心。如果学会了就会对代码的理解有进了一步,恭喜你~ 如果文章对你有帮助的话不妨点个赞。

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

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

相关文章

8.20T3 无损加密(线性代数转LGV+状压dp+高维前缀和)

http://cplusoj.com/d/senior/p/NODSX2301C 对于式子&#xff1a; 这个神秘的线性代数形式比较难处理&#xff0c;但我们可以考虑其组合意义。行列式现存的可用组合意义之一就是LGV&#xff08;矩阵式不太可用&#xff09; 先把原先的矩阵转化为一个有向图。现在我们要构造一…

笔记本电脑无线网卡突然没有了

目录 笔记本电脑无线网卡突然没有了最优解决方案 笔记本电脑无线网卡突然没有了 记录一次笔记本无线网卡突然没有了的解决方案 显示黄色感叹号&#xff0c;试了几个安装驱动的软件都不行 最优解决方案 找到网卡的厂商官网&#xff0c;官网上下载驱动 比如我的无线网卡是Int…

【Hot100】LeetCode—146. LRU 缓存

目录 1-思路1-1 LRU知识点1-2 实现思路LRU的子数据结构① 双向链表 DLinkedNode 结点定义② 其他字段 LRU实现的方法① 初始化——LRUCache中初始化② public int get(int key) 取元素方法③ public void put(int key, int value) 存元素方法 2-实现⭐146. LRU 缓存——题解思路…

rufus制作ubantu的U盘安装介质时,rufus界面上的分区类型选什么?

rufus制作ubantu的U盘安装介质时&#xff0c;rufus软件界面上的分区类型选什么(如下图&#xff09;&#xff1f; 在使用Rufus制作Ubuntu的U盘安装介质时&#xff0c;分区类型的选择取决于我们的计算机的引导方式。 以下是具体的选择建议&#xff1a; 1、查看计算机的引导方式…

JAVA设计模式之【单例模式】

1 类图 2 饿汉式单例 例如&#xff1a;静态块、静态成员 2.1 概念 类加载的时候就立即初始化&#xff0c;并且创建单例对象 2.2 优点 没有加任何的锁、执行效率比较高 2.3 缺点 类加载的时候就初始化&#xff0c;不管用与不用都占着空间&#xff0c;浪费了内存。 3 懒汉…

Java之迭代器的使用

Java之迭代器的使用 摘要基础知识List迭代器Map迭代器 摘要 本博客主要讲解容器的迭代器的使用&#xff0c;包括List、Set和Map等容器 基础知识 这是类的继承关系图 迭代器的原理(一开始迭代器并不指向任何有效元素)&#xff1a; List迭代器 public class TestIterator …

VMware vSphere Client无法访问和连接ESXi虚拟主机解决思路

文章目录 前言1. 问题现象2. 问题原因3. 解决方法4. 参考文章 前言 注意 : 可以先看看参考文章那里&#xff0c;在回过来看 1 、 2 、3 1. 问题现象 版本&#xff1a;VMware vCenter Server 5.5.0 build-2442329 问题描述&#xff1a;用VMware vSphere Client 登录ESXI主机出…

【Linux —— 线程互斥】

Linux —— 线程互斥 1. 临界资源与临界区2. 互斥的定义3. 原子性4. 互斥量(Mutex)5. 互斥的实现示例 1. 临界资源与临界区 临界资源: 指的是多个线程或进程共享的资源&#xff0c;例如全局变量、文件、数据库等。由于这些资源的共享&#xff0c;可能会导致数据不一致或程序崩…

git commit 时发生:fatal: cannot lock HEAD ref

.git目录探析_.git文件在哪-CSDN博客https://blog.csdn.net/luofeng457/article/details/117577275 tree .git .git ├── branches ├── COMMIT_EDITMSG ├── config ├── description ├── FETCH_HEAD ├── HEAD ├── hooks │ ├── applypatch-msg.sample…

STM32的GPIO

GPIO基本控制 GPIO(General-Purpose input/output,通用输入/输出接口) 用于感知外部信号&#xff08;输入模式&#xff09;和控制外部设备&#xff08;输出模式&#xff09; 简单模块&#xff1a;LED,按键&#xff0c;蜂鸣器&#xff0c;温度传感器&#xff0c;使用一个GPIO…

qt-PLC可视化编辑器

qt-PLC可视化编辑器 一、演示效果二、核心代码三、下载链接 一、演示效果 二、核心代码 #include "diagramitem.h" #include "arrow.h"#include <QDebug> #include <QGraphicsScene> #include <QGraphicsSceneContextMenuEvent> #includ…

1 Kubeflow总体介绍-学习笔记

1 什么是 Kubeflow Kubeflow 是一个开源项目社区和生态系统&#xff0c;支持机器学习 (ML) 生命周期中的每个阶段 &#xff0c;并支持相关的开源 工具和框架&#xff0c;Kubeflow 使 Kubernetes 上的 AI/ML 变得简单、可移植且可扩展。 Kubeflow 都能提供模块化、可扩展的工具…

C语言第17篇

1.在C语言中,全局变量的存储类别是_________. A) static B) extern C) void D) register 提示&#xff1a;extern adj.外来的 register n.登记表&#xff0c;v.登记 提示与本题无关 2.在一个C源程序文件中,要定义一个只允许本源文件中所有函数使用的全局变…

JimuReport 积木报表 v1.8.0 版本发布,开源可视化报表

项目介绍 一款免费的数据可视化报表工具&#xff0c;含报表和大屏设计&#xff0c;像搭建积木一样在线设计报表&#xff01;功能涵盖&#xff0c;数据报表、打印设计、图表报表、大屏设计等&#xff01; Web 版报表设计器&#xff0c;类似于excel操作风格&#xff0c;通过拖拽完…

【Python】成功解决 NameError: name ‘reload‘ is not defined

【Python】成功解决 NameError: name ‘reload’ is not defined 下滑即可查看博客内容 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地&#xff01;&#x1f387; &#x1f393; 博主简介&#xff1a;985高校…

【学习笔记】7、存储器、复杂可编程器件和现场可编程门阵列

可编程逻辑器件PLD复杂可编程逻辑器件CPLD现场可编程门阵列FPGA 7.1 只读存储器&#xff08;ROM&#xff09; 7.1.1 ROM的结构 ROM存储器 存储阵列 地址译码器 输出控制电路 存储阵列&#xff0c;由许多存储单元&#xff08;1bit&#xff09;组成。每次读出一组数据&…

HTML实现俄罗斯方块

本篇文章主要讲使用HTML、CSS和JavaScript实现一个简单的俄罗斯方块游戏&#xff0c;包含基本的游戏逻辑、行消除功能以及暂停和继续游戏的控制。 使用工具 本篇文章有用到ChatGPT-4o代码纠错&#xff0c;国内免翻且稳定&#xff0c;感兴趣的大佬试试。 传送门&#xff1a;36…

认知杂谈20

今天分享 有人说的一段争议性的话 I I 程序员的高薪舒适圈&#xff1a;光鲜背后的挑战 一说起程序员这个职业&#xff0c;很多人马上就会想到高薪&#xff0c;觉得他们过着白领的生活。确实&#xff0c;程序员一般都能拿到比好多行业都高的工资&#xff0c;工作时间也比较稳…

谷粒商城实战笔记-232-商城业务-认证服务-框架效果演示-xxl-sso-徐雪里

文章目录 一&#xff0c;膜拜大神许雪里二&#xff0c;用开源框架xxl-sso演示单点登录1&#xff0c;我是老板&#xff0c;我有三个网站2&#xff0c;配置域名3&#xff0c;下载xxl-sso代码4&#xff0c;服务规划5&#xff0c;配置修改5.1 xxl-sso-server redis配置修改5.1 xxl-…

使用FModel提取黑神话悟空的资产

使用FModel提取黑神话悟空的资产 前言设置效果展示闲聊可能遇到的问题没有相应的UE引擎版本选项 前言 黑神话悟空昨天上线了&#xff0c;解个包looklook。 本文内容比较简洁&#xff0c;仅介绍解包黑神话所需的专项配置&#xff0c;关于FModel的基础使用流程&#xff0c;请见…