C++开发基础——可变参数与可变参数模板

一,可变参数

1.基础概念

可变参数在C语言和C++语言编程中都有应用。

可变参数的含义是:在函数传参的时候,参数的数量、类型都是可变的,不确定的。

在C语言中,应用到可变参数的是可变参数函数可变参数的宏

在C++语言中,C++11标准提供了两种使用可变参数的方式:

1.如果可变参数的参数类型相同,可以使用标准库中的initializer_list

2.如果可变参数的参数类型不同,可以使用可变参数模板

C语言中,在定义可变参数函数时,使用省略号"..."表示参数是可变的。

简单代码样例如下:

void printf(const char* format, …);

可变参数的使用可以让代码结构更精简。

2.可变参数相关的宏定义

在C语言中,一般需要借助相关的宏定义来实现可变参数,常见的宏定义如下:

va_arg:每一次调用va_arg会获取当前的参数,并自动更新指向下一个可变参数。

va_start:获得可变参数列表的第一个参数,开始使用可变参数列表。

va_end:结束对可变函数列表的遍历,释放va_list。

va_list:存储可变参数列表的具体信息。

    简单介绍就是,va_start用于开始使用可变参数,va_arg用于获得下一个可变参数,va_end用于释放va_list。

    它们都包含在头文件"<stdarg.h>"中

这些宏定义在具体应用时的语法如下:

type va_arg(va_list arg_ptr,type
);void va_end(va_list arg_ptr
);void va_start(va_list arg_ptr,prev_param
);void va_start(arg_ptr
); // (deprecated Pre-ANSI C89 standardization version)

注意,如果自定义参数和可变参数同时在函数中出现,为了不导致编译出错,将可变参数放在形参列表的最后一个位置。

void func(char parm_1, int parm_2, ...);

完整代码样例:

#include <stdio.h>  
#include <stdarg.h>    
void vout(int max, ...)
{va_list arg_ptr;int args = 0;char* days[7];va_start(arg_ptr, max);while (args < max){days[args] = va_arg(arg_ptr, char*);printf("Day:  %s  \n", days[args++]);}va_end(arg_ptr);
}
int main(void)
{vout(3, "Sat", "Sun", "Mon");printf("\n");vout(5, "Mon", "Tues", "Wed", "Thurs", "Fri");
}

运行结果:

Day:  Sat
Day:  Sun
Day:  MonDay:  Mon
Day:  Tues
Day:  Wed
Day:  Thurs
Day:  Fri

3.预定义标识符_VA_ARGS__

    对于可变参数相关的代码编写,除了使用省略号来表示可变参数列表,也可以使用__VA_ARGS__ 预定义标识符来表示可变参列表。

    该语法在C99标准中被引入,可以简单了解一下。

    可以用"__VA_ARGS__"表示"..."位置的所有参数,用法如下:

#define PRINT(...) printf(__VA_ARGS__)

完整代码样例:

#include <stdio.h>
#define DEBUG
#ifdef DEBUG
#define PRINT(...)  fprintf(stderr, __VA_ARGS__)
#else
#define PRINT(...)  printf(__VA_ARGS__)
#endif
int main(void)
{const char* s = "abc";int n = 123;PRINT("%d\n", n);PRINT("%s %d\n", s, n);
}

 运行结果:

123
abc 123

二,标准库模板initializer_list

initializer_list模板在函数声明中可以代表可变参数列表。

initializer_list中的参数可以使用迭代器来访问。

initializer_list实例中传入参数时需要使用{}把多个参数括起来。

代码样例:

initializer_list<int> i1{ 1, 2, 3, 4 };

Demo1:  初始化类成员

#include <iostream>
#include <vector>
#include <initializer_list>
class Point {std::vector<int> arr;
public://Constructor accepts a initializer_list as argumentPoint(const std::initializer_list<int>& list) : arr(list){}void display() {for (int i : arr)std::cout << i << " , ";std::cout << std::endl;}
};
int main() {Point pointobj({ 1, 2, 3, 4, 5 });pointobj.display();return 0;
}

运行结果:

1 , 2 , 3 , 4 , 5 ,

Demo2:结合lambda表达式一起使用

#include <iostream>
#include <initializer_list>
using namespace std;
template<typename... Args>
void print(Args... args)
{std::initializer_list<int>{([&] { cout << args << " "; }(), 0)...};
}
int main()
{print(1, 2, "3A", 4);return 0;
}

运行结果:

1 2 3A 4

三,可变参数模板

1.基础概念

可变参数模板是支持任意数量和类型的参数的类模板或函数模板。

在可变参数模板中,可变数目和类型的参数列表被称为参数包(parameter pack)。

可变参数模板的参数包,分为模板参数包(template parameter pack)和函数参数包(function parameter pack)。

在模板参数位置的可变参数被称为模板参数包,在函数参数位置的可变参数被称为函数参数包。

可以使用sizeof...运算符获取参数包中具体的参数数量。

样例如下: 

//Args是一个模板参数包;args是一个函数参数包
template <typename... Args>
void func(Args... args);

如上所示,在一个模板参数列表中:

class...或typename...表示接下来的参数是零个或多个类型列表。

类型名...表示接下来的参数是零个或多个给定类型的函数参数列表。

比较一下"typename T"和"typename.. Args":

Args和T的差别是,T与一种类型匹配,而Args与任意数量(包括零)的类型匹配。

完整代码样例:

Demo1:

#include <iostream>
template <typename T>
void printAllImpl(T item) {std::cout << item << ' ';
}
template <typename T, typename ...Args>
void printAllImpl(T item, Args ... args) {printAllImpl(item);printAllImpl(args...);
}
template <typename... Args>
void printAll(Args&&... args) {printAllImpl(std::forward<Args>(args) ...);std::cout << '\n';
}
int main() {printAll(3, 2, 1);printAll(8.2, 2, 1.1, "A");printAll(23, 32, 8, 11, 9);
}

运行结果: 

3 2 1
8.2 2 1.1 A
23 32 8 11 9

2.参数包的递归解析

可变参数列表中,参数包的展开方式为递归展开,即将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给相同函数递归调用,以此类推,直到参数列表为空。

代码样例:

#include <iostream>
template<typename T, typename... Args>
void show_list(T value, Args... args)
{std::cout << value << ", ";show_list(args...); //递归调用
}
int main()
{int n = 2;double m = 3.0;std::string str = "test";show_list(1, n, m);show_list(1, n, m * m, str);return 0;
}

以上代码在VS2019中运行时,会报以下编译错误:

“show_list”: 未找到匹配的重载函数
“void show_list(T,Args...)”: 应输入2个参数,却提供了0个

    出现以上问题的原因是,可变参数函数模板通常是递归的。函数在第一次调用时,会使用参数包中的第一个实参,然后递归调用自身来陆续使用参数包中的剩余实参。为了终止递归,我们还需要定义一个非可变参数的函数模板或者普通函数。

    以下代码都包含终止递归的函数模板。

    Demo1: 

#include <iostream>//用来终止递归并处理参数包中最后一个元素
template<typename T>
void show_list(T value)
{std::cout << value << ", ";
}//参数包中除了最后一个元素之外的其他元素都会调用这个版本的show_list
template<typename T, typename... Args>
void show_list(T value, Args... args)  
{std::cout << value << ", ";show_list(args...); //递归调用
}int main()
{int n = 2;double m = 3.0;std::string str = "test";show_list(1, n, m);show_list(1, n, m * m, str);return 0;
}

运行结果:

1, 2, 3, 1, 2, 9, test,

Demo2:

#include <iostream>
void tprintf(const char* format) //终止递归调用
{std::cout << format;
}
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs)
{for (; *format != '\0'; format++){if (*format == '%'){std::cout << value;tprintf(format + 1, Fargs...); //递归调用return;}std::cout << *format;}
}
int main()
{tprintf("% world% %\n", "Hello", '!', 123);
}

运行结果:

Hello world! 123

特殊情况,当不涉及"typename T"的使用时,可以不需要单独定义一个非可变参数的函数模板来终止递归。

Demo3:

#include <iostream>
using namespace std;
template<typename... Argv>
void print_func(Argv... argv)
{cout << "print_func() is called with "<< sizeof...(Argv)<< " argument(s)." << endl;
}
int main(void)
{print_func();print_func(4, "a");print_func("a", "b", "c");return 0;
}

运行结果:

print_func() is called with 0 argument(s).
print_func() is called with 2 argument(s).
print_func() is called with 3 argument(s).

3.参数包展开过程拆解

演示代码:

#include <iostream>
using namespace std;
void print()
{cout << "I am empty.\n";
}
template <typename T, typename... Types>
void print(T var1, Types... var2)
{cout << var1 << endl;print(var2...);
}
int main()
{print(1, 2, 3.14, "test");return 0;
}

过程拆解: 

main函数中,第一次调用print,传递的实参:1,参数包剩余元素:2, 3.14, "test"。第一次递归调用print,传递的实参:2,参数包剩余元素:3.14, "test"第二次递归调用print,传递的实参:3.14,参数包剩余元素:"test"第三次递归调用print,传递的实参:"test",参数包中的元素已全部用完。由于参数包中的元素为空,退出递归,最后调用的是具体函数print()。

运行结果:

1
2
3.14
test
I am empty.

4.sizeof...运算符

    由于带有"typename T"参数的可变参数的模板函数,总是需要再定义一个同名的模板函数或者普通函数来搭配使用,使得代码特别重复。

    为了解决以上问题,可以使用"sizeof..."运算符来保证,在不重复定义同名函数的情况下让递归退出。

    "sizeof..."运算符可以判断参数包中的元素数量。

    退出递归的方式: 判断当参数包的元素个数为零时,退出函数调用。

sizeof...用法演示:

#include <cassert>
#include <stdio.h>
template<class...A>
int func(A...arg) {return sizeof...(arg);
}
int main(void) {if (func<int>(1, 2, 3, 4, 5) == 5) {printf("the num of arg is 5");}return 0;
}

运行结果: 

the num of arg is 5

sizeof...在结束递归中的使用

Demo1:

#include <iostream>
template<typename T, typename... Args>
void print_2(T value1, Args... args) {std::cout << value1 << ", ";if(sizeof...(args) > 0) {print_2(args...);}
}
int main()
{print_2(1, 2, "A");
}

以上用法无法导致递归终止,而且还会引起编译报错,原因是if判断无法在该函数模板中生效。为了解决以上问题,C++17标准中引入了编译期if条件判断的表达式"if constexpr"。

Demo2:

#include <iostream>
template<typename T, typename... Args>
void print_2(T value1, Args... args) {std::cout << value1 << ", ";if constexpr (sizeof...(args) > 0) {print_2(args...);}
}
int main()
{print_2(1, 2, "A");
}

运行结果: 

1, 2, A,

四,参考阅读

《C++17入门经典》

《C++ primer》

《深入理解C++11》

https://www.sandordargo.com/blog/2023/05/03/variadic-functions-vs-variadic-templates

https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/va-arg-va-copy-va-end-va-start?view=msvc-170

https://www.ibm.com/docs/en/zos/2.3.0?topic=lf-va-arg-va-copy-va-end-va-start-access-function-arguments

https://www.sandordargo.com/blog/2023/05/03/variadic-functions-vs-variadic-templates

https://en.cppreference.com/w/cpp/language/parameter_pack

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

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

相关文章

阿里云2核4G4M轻量应用服务器价格165元一年

阿里云优惠活动&#xff0c;2核4G4M轻量应用服务器价格165元一年&#xff0c;4Mbps带宽下载速度峰值可达512KB/秒&#xff0c;系统盘是60GB高效云盘&#xff0c;不限制月流量&#xff0c;2核2G3M带宽轻量服务器一年87元12个月&#xff0c;在阿里云CLUB中心查看 aliyun.club 当前…

数据结构—稀疏多项式相加

利用链表实现两个稀疏多项式相加。 代码 #include <iostream> using namespace std;// 定义多项式项结构体 typedef struct {int x; // 系数int y; // 指数 } Elemtype;// 定义链表节点结构体 typedef struct Node {Elemtype data;struct Node* next; } *LinkList, N…

数据结构中单向链表(无头)的学习

一.数据结构 1.定义 一组用来保存一种或者多种特定关系的数据的集合&#xff08;组织和存储数据&#xff09; 程序的设计&#xff1a;将现实中大量而复杂的问题以特定的数据类型和特定的存储结构存储在内存中&#xff0c; 并在此基础上实现某个特定的功能的操…

VSCode + PicGo + Github 实现markdown图床管理

目录 PicGo客户端VSvode插件 PicGo客户端 PicGo 是一个图片上传管理工具 官网&#xff1a;https://molunerfinn.com/PicGo/ github图传使用说明&#xff1a;https://picgo.github.io/PicGo-Doc/zh/guide/config.html#GitHub图床 步骤&#xff1a; 1、创建一个github公开仓库…

elementUI 中使用 Popover 弹出框 el-popover自 如何自定义样式 ?(出现 popper-class 不生效,如何解决 ?)

动态赋值 &#xff1f; 通过 :content 动态赋值 <el-popoverplacement"right"title"驳回原因:"width"200"trigger"hover"popper-class"custom-popover":content noPassReason ><el-button slot"reference…

vmap与kmap介绍

vmap() 可以用来将多个物理页长期映射到一个连续的虚拟空间。它需要全局同步来解除映射。 kmap() 用来对单个页面进行短时间的映射&#xff0c;对抢占或迁移没有限制。 它会带来开销&#xff0c;因为映射空间是受限制的&#xff0c;并且受到全局锁的保护&#xff0c;以实现…

小程序搜索排名优化二三事

小程序的优化主要是排名优化和性能优化两个版块。性能优化这方面主要靠开发者自己完善&#xff0c;我们团队提供的服务就是把产品的排名打上去&#xff0c;获得更多的自然流量&#xff0c;实现盈利。 如何提升小程序的搜索排名主要从如下几个方面出发&#xff1a; 首先要知道…

2023新版mapinfo美化电子地图 新版2013Arcgis shp电子地图 下载

2023新版MapInfo和电子地图美化&#xff0c;以及2013版ArcGIS的SHP电子地图设计&#xff0c;是地理信息系统&#xff08;GIS&#xff09;领域中的两个重要话题。下面将分别对这两个主题进行描述。 样图&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1WB4AGsycyBGagVq5…

文心一言赋能问卷生成,打造高效问卷调研工具

当前&#xff0c;各种大语言模型&#xff08;LLM&#xff0c;Large Language Model&#xff09;井喷式发展&#xff0c;基于LLM的应用也不断涌现。但是&#xff0c;当开发者基于LLM开发下游应用时&#xff0c;LLM直接生成的结果在格式、内容等方面都存在许多不确定因素&#xf…

【数据结构和算法初阶(C语言)】二叉树的顺序结构--堆的实现/堆排序/topk问题详解---二叉树学习日记②

目录 ​编辑 1.二叉树的顺序结构及实现 1.1 二叉树的顺序结构 2 堆的概念及结构 3 堆的实现 3.1堆的代码定义 3.2堆插入数据 3.3打印堆数据 3.4堆的数据的删除 3.5获取根部数据 3.6判断堆是否为空 3.7 堆的销毁 4.建堆以及堆排序 4.1 升序建大堆&#xff0c;降序建小堆 4.2堆…

【C语言步行梯】一级指针、二级指针、指针数组等 | 指针详谈

&#x1f3af;每日努力一点点&#xff0c;技术进步看得见 &#x1f3e0;专栏介绍&#xff1a;【C语言步行梯】专栏用于介绍C语言相关内容&#xff0c;每篇文章将通过图片代码片段网络相关题目的方式编写&#xff0c;欢迎订阅~~ 文章目录 什么是指针&#xff1f;指针的大小指针类…

SpringMVC | SpringMVC中的“JSON数据交互“ 和“RESTful支持“

目录: 1.JSON 数据交互1.1 JSON概述1.2 JSON的“数据结构”对象结构数组结构 1.3 JSON的“数据转换” (JSON交互) 作者简介 &#xff1a;一只大皮卡丘&#xff0c;计算机专业学生&#xff0c;正在努力学习、努力敲代码中! 让我们一起继续努力学习&#xff01; 该文章参考学习教…

HUAWEI Pocket 2外屏实时查看App动态,小小窗口大便捷

当我们点外卖、等候飞机时&#xff0c;不少人习惯频繁点亮手机查看外卖配送进度、值机时间。 这时候&#xff0c;手机亮屏、解锁、打开对应App查看状态对于我们来说就显得非常繁琐。而华为Pocket 2结合HarmonyOS 4系统的实况窗功能&#xff0c;与常显外屏的搭配使用&#xff0…

微服务技术栈SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式(五):分布式搜索 ES-下

文章目录 一、数据聚合1.1 聚合种类1.2 DSL实现聚合1.3 RestAPI实现聚合1.4 演示&#xff1a;多条件聚合 二、自动补全2.1 拼音分词器2.2 自定义分词器2.3 DSL自动补全查询2.5 实现酒店搜索框自动补全2.5.1 修改酒店索引库数据结构2.5.2 RestAPI实现自动补全查询2.5.3 实战 三、…

Games101课程笔记1--图形学简介

1. 图形学应用 电子游戏电影特效动画计算机辅助设计虚拟化虚拟现实VR数字插图模拟GUI图形用户接口字体设计 2. 为什么学图形学 基础知识挑战 创建现实与虚拟世界的交互需要理解物理世界的各个方面新的展示方法科技 技术上的挑战 数学上的投影曲面平面物理上的光照着色操作和…

CSS案例-2.简单版侧边栏练习

效果 知识点 标签显示模式 块级元素 block-level 常见元素:<h1>~<h6>、<p>、<div>、<ul>、<ol>、<li>等。 特点: 独占一行长度、宽度、边距都可以控制宽度默认是容器(父级宽度)的100%是一个容器及盒子,里面可以放行内或者…

Docker部署TeamCity来完成内部CI、CD流程

使用TeamCity来完成内部CI、CD流程 本篇教程主要讲解基于容器服务搭建TeamCity服务&#xff0c;并且完成内部项目的CI流程配置。至于完整的DevOps&#xff0c;我们后续独立探讨。 一个简单的CI、CD流程 以下分享一个简单的CI、CD流程&#xff08;仅供参考&#xff09;&#…

HCIA_IP路由基础问题?

目录 1. 什么是路由&#xff1f;2. 什么是路由器&#xff1f;3. 什么是路由信息&#xff1f;4. 路由器信息和路由表的区别&#xff1f;5. 路由表的生成方式&#xff1f;6.直连路由生效条件是什么&#xff1f;7.Inloopback0是什么接口&#xff1f;8.最优路由选择的原则&#xff…

SCI一区 | Matlab实现RIME-TCN-BiGRU-Attention霜冰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测

SCI一区 | Matlab实现RIME-TCN-BiGRU-Attention霜冰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测 目录 SCI一区 | Matlab实现RIME-TCN-BiGRU-Attention霜冰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测预测效果基本介绍模型描述程…

DEYOv2: Rank Feature with Greedy Matchingfor End-to-End Object Detection

摘要 与前代类似&#xff0c; DEYOv2 采用渐进式推理方法 来加速模型训练并提高性能。该研究深入探讨了一对一匹配在优化器中的局限性&#xff0c;并提出了有效解决该问题的解决方案&#xff0c;如Rank 特征和贪婪匹配 。这种方法使DEYOv2的第三阶段能够最大限度地从第一和第二…