C++11 可变参数模版详解

news2024/11/20 8:50:50
  • 本文已同步到洛谷专栏

可变参数模板

函数模板

给没有接触过函数模板的同学讲一下函数模板。

首先考虑下面一段代码。

#include<bits/stdc++.h>
using namespace std;

//Some function definitions...

int main(){
	out(1);
	out(1.4);
	out(970132ll);
	out('H');
	out("Hero_Broom");
	return 0;
}

我们计划使用 out 函数来输出给入的变量。在上面的代码中,如果想要输出上面的信息,我们需要写五个 out 函数,来对应每一种类型。这是非常不方便的,如果我们还想要支持更多类型,我们岂不是要写一大堆 out 函数?

这就引入了模板函数了 —— 它可以接受任何类型,具体实现如下:

#include<bits/stdc++.h>
using namespace std;

template <class T>
void out(T v){
	cout<<v<<endl;
}

int main(){
	out(1);
	out(1.4);
	out(970132LL);
	out('H');
	out("Hero_Broom");
	return 0;
}

其中的 class 可替换为 typename

我们只需要记住这条语句即可,接下来函数中的 T 都可以看做是一个类型,传入的值为 1.4 1.4 1.4T 就为 f l o a t float float,传入的值为 970132 L L 970132LL 970132LLT 就为 l o n g   l o n g long\ long long long

在一个函数中,我们可以同时定义多种类型,例如下面的程序:

#include<bits/stdc++.h>
using namespace std;

template <class T,class S>
double devide(T a,S b){
	return a/b;
}

int main(){
	cout<<devide(1,4)<<endl;
	cout<<devide(1LL,4.0)<<endl;
	return 0;
}

输出:

0
0.25

--------------------------------
Process exited after 0.03401 seconds with return value 0
请按任意键继续. . .

学习完了函数模板,就可以看可变参数模版了。

引入

在 C++11 之前,类模板和函数模板只支持固定的参数。也就是说,你不能在同一个函数里改变参数的数目,就比如下面的代码:

#include<bits/stdc++.h>
using namespace std;

//The definition of function 'show_list'.

int main(){
    show_list(1);
    show_list("Hero_Broom",970132);
    show_list(1234567,'\n',"It is just a test!!!");
    return 0;
}

如果我们想要让 show_list 函数输出传入的所有参数,并在每个参数的输出之间加上空格,那么我们就必须定义三个甚至更多的函数,来分别对应传入参数个数有一个,两个和三个的情况。

template <typename A>
void show_list(A a){
	cout<<a<<" ";
}

template <typename A,typename B>
void show_list(A a,B b){
	cout<<a<<" "<<b<<" ";
}

template <typename A,typename B,typename C>
void show_list(A a,B b,C c){
	cout<<a<<" "<<b<<" "<<c<<" ";
}

如果只是看上面的函数的话,可能你觉得不就是多写几个函数吗,那如果传入的参数有十个,函数写起来就会非常麻烦。

在 C++11 以后,模板功能被增强,允许模板定义中包含零到任意个模板参数,这也就是可变参数模板了。可变参数模板的加入使得 C++11 的功能变得更加强大,而由此也带来了许多神奇的用法。

可变参数模板的用法

可变参数模板与普通模板的写法比较类似,只需要在 typename 后面加上省略号 ... 即可。我们称代码中 args 为一个形参参数包

template <typename T,typename... types>
void show_list(T val,types... args){
    
}

需要注意的是,这里的 types... args 传入的参数个数是大于等于零个的,而在前面还有一个 val 参数。加上这个 val,正割函数可接受的参数个数范围为 [ 1 , + ∞ ) \left [1 , + \infty \right ) [1,+)

也就是说如果我们使用下面的语句:

show_list();

程序就会报错。所以需要再加上一个额外的定义,专门处理没有传入参数的情况。

void show_list(){
    cout<<endl;
}

如果我们要获取参数包的大小,则可以通过 sizeof 实现,具体代码如下:

template <typename T,typename... types>
void show_list(T val,types... args){
    printf("The size of the data pack is: %d\n",sizeof...(args));
}

那么有人可能就想到了:既然我们已经知道参数包的大小了,我们能不能向数组一样遍历它来输出呢?由此我们写出了下面的代码:

template <typename... types>
void show_list(types... args){
    for(int i=0;i<sizeof...(args);i++){
		cout<<args[i]<<" ";
	}
}

但遗憾的是,这段代码会出现报错信息,我们不能像遍历数组一样去遍历参数包。

[Error] parameter packs not expanded with '...':

所以我们就需要用到参数包的递归展开来实现输出的功能了。

参数包的展开

首先注意到上面的函数中除了参数包 args 外,我们还在前面单独提出一个参数 T val,那你可能就会问了:为什么我们不能直接把数据全部丢进参数包里面,还要单独拎出一个 val 出来呢。先来看看我们是怎样实现参数包的输出的:

  1. 输出参数列表中第一个参数;

  2. 递归输出后面的参数,具体就要用到我们刚才说到的参数包了。

那看到这里你可能就明白了,我们要先输出第一个参数的话,就不能只使用参数包了,因为它是不能直接像数组一样访问的。

具体的代码实现如下所示,可以看到函数中先输出了第一个参数,然后再 show_list 输出剩下参数包中的参数。

#include<bits/stdc++.h>
using namespace std;

void show_list(){
	cout<<endl;
}

template <class T,class... types>
void show_list(const T val,const types... args){
	cout<<val<<" ";
	show_list(args...);
}

int main(){
    show_list(1);
    show_list("Hero_Broom",970132);
    show_list(1234567,'\n',"It is just a test!!!");
    return 0;
}

输出:

1
Hero_Broom 970132
1234567
 It is just a test!!!

--------------------------------
Process exited after 0.03312 seconds with return value 0
请按任意键继续. . .

~~ 那么可能就有细心的同学发现了,~~ 为什么每一条输出之间都有一个换行呢,在 show_list 之间也没有换行啊?这时候,我们可以拆开来一步一步看。

假设让我们调用 show_list(1234567,'a',"It is just a test!!!")

  1. show_list(1234567,'a',"It is just a test!!!") 输出 1234567 ,递归 show_list('a',"It is just a test!!!")

  2. show_list('a',"It is just a test!!!") 输出 1234567 s ,递归 show_list("It is just a test!!!")

  3. show_list("It is just a test!!!") 输出 1234567 a It is just a test!!! ,递归 show_list()

  4. show_list(),什么都没有,那输出什么呢,前面我们定义了 void show_list(){cout<<endl;},所以这里就会输出一个换行,并结束递归。

所以这就是为什么会有这个换行符的原因,也同时解决了如何结束递归的问题。

也就是说,如果在上面定义无参的 show_list 时去掉 cout<<endl; 这句,换行也就不会出现了。

带参递归终止函数

上面的递归终止函数,也就是那一个无参的 show_list 函数。它是没有参数的,所以称它为无参递归终止函数带参递归终止函数带有一个参数,用途和无参递归终止函数一样,都用来让递归终止;原理也和上面无参递归终止函数类似,所以我们可以直接看代码:

#include<bits/stdc++.h>
using namespace std;

void show_list(){
	cout<<endl;
}

template <class T>
void show_list(T val){
	cout<<val<<" ";
}

template <class T,class... types>
void show_list(const T val,const types... args){
	cout<<val<<" ";
	show_list(args...);
}

int main(){
    show_list(1);
    show_list("Hero_Broom",970132);
    show_list(1234567,'\n',"It is just a test!!!");
    return 0;
}

输出:

1 Hero_Broom 970132 1234567
 It is just a test!!!
--------------------------------
Process exited after 0.03364 seconds with return value 0
请按任意键继续. . .

可以看到,这里只有一个换行,而这个换行是上面代码中 show_list(1234567,'\n',"It is just a test!!!") 中输出的,那么上面的代码中为什么加了一个带参递归终止函数就没有输出换行了呢,我们也可以一步一步地模拟。

还是以 show_list(1234567,'a',"It is just a test!!!") 为例:

  1. show_list(1234567,'a',"It is just a test!!!") 输出 1234567 ,递归 show_list('a',"It is just a test!!!")

  2. show_list('a',"It is just a test!!!") 输出 1234567 s ,递归 show_list("It is just a test!!!")

  3. show_list("It is just a test!!!"),这个时候条用的就是上面的带参归终止函数了。所以输出完 1234567 a It is just a test!!! 之后就不会再递归了。

在这个过程中,可以看到相较于上面少了第四步,因此就不会再使用无参递归终止函数,也就不会输出换行了。

所以需要注意的是,编写带参递归终止函数需要使用函数模板,因为我们并不知道最后一个参数是什么类型的。

但这种方法有一个弊端就是,我们在调用 show_list 函数时必须传入至少一个参数,否则就会报错。因为此时无论是调用递归终止函数还是展开函数,都需要至少传入一个参数。

通过逗号表达式展开参数包

这个部分比较难理解,我尽量讲清楚一些。我自己学的时候都有点懵。

在学习用逗号表达式展开参数包前,我们当然需要先了解逗号运算符的用法和性质。

  • 逗号表达式会从左到右依次计算各个表达式

  • 逗号表达式会将最后一个表达式的值作为返回值返回。

这个时候,我们来看一下代码。

#include<bits/stdc++.h>
using namespace std;

template<class T>
void out(const T& t){
	cout<<t<<" ";
}

template<class ...types>
void show_list(types... args){
	int arr[] = { (out(args), 0)... };
	cout<<endl;
}

int main(){
	show_list(1234567,'a',"Hero_Broom");
	return 0;
}

我们一步一步来看:

  1. 首先,将逗号表达式的最后一个表达式设为 0 0 0,这样整个逗号表达式的返回值就是 0 0 0,正好对应 a r r arr arr 数组的类型 i n t int int

  2. 然后将逗号表达式的第一个表达式设为输出函数,用来输出参数包里的每一个参数

  3. 在初始化数组时,会从左到右计算初始化列表里的每一个表达式的值(在这里都为零)。我们并不关心最后 a r r arr arr 数组里的数,关键在于初始化数组时对于参数包里每一个参数的输出

上面没看懂的一定要多看几遍。

在这里,{ (out(args), 0)... } 相当于对参数包里每一个参数都执行一遍 out 函数,相当于:

{   (   o u t ( a r g 1 ) , 0   )   ,   (   o u t ( a r g 2 ) , 0   )   ,   . . .   , (   o u t ( a r g n ) , 0   )   ,   } \{ \ (\ out(arg_1),0\ )\ ,\ (\ out(arg_2),0\ )\ ,\ ...\ ,(\ out(arg_n),0\ )\ ,\ \} { ( out(arg1),0 ) , ( out(arg2),0 ) , ... ,( out(argn),0 ) , }

程序在初始化 a r r arr arr 数组时,会先计算 (out(arg1),0) 的值。这时候就顺便运行了一遍 out 函数,把第一个参数的值输出了,而 arr[0] 的值就变成了 0 0 0

这样,在初始化整个 a r r arr arr 数组时,程序就把传入的参数包中的参数顺便输出了。

当然,如果你觉得使用 (out(args),0) 太麻烦的话,也可以写一个返回 i n t int intout 函数,因为本质上写这些的目的就是让初始化值为 i n t int int 类型。

#include<bits/stdc++.h>
using namespace std;

template<class T>
int out(const T& t){
	cout<<t<<" ";
	return 0;
}

template<class ...types>
void show_list(types... args){
	int arr[] = {out(args)...};
	cout<<endl;
}

int main(){
	show_list(1234567,'a',"Hero_Broom");
	return 0;
}

输出:

1234567 a Hero_Broom

--------------------------------
Process exited after 0.1315 seconds with return value 0
请按任意键继续. . .

与上面使用逗号表达式效果是一样的。

可变参数模版的应用

当我们开发程 (you) 序 (xi) 时,经常会需要日志。日志的形式多种多样,有时候只需要记录一个信息,而有时候要记录很多信息,所以这个时候使用可变参数模板实在适合不过的了。

#include<bits/stdc++.h>
#include<windows.h>
using namespace std;

ofstream off;

void show_list(){
	off<<endl;
}

template <class T,class... types>
void show_list(const T val,const types... args){
	off<<val<<" ";
	show_list(args...);
}

template <class... types>
void Write_Log(const char* file_name,types... args){
	off.open(file_name);
	time_t ti=time(0);
	tm* _t=localtime(&ti);
	off<<"["<<_t->tm_year+1900<<"->"<<_t->tm_mon<<"->"<<_t->tm_mday<<"\t";
	off<<_t->tm_hour<<":"<<_t->tm_min<<":"<<_t->tm_sec<<"]:";
	show_list(args...);
}

int main(){
    Write_Log("log.log","The programme begins.");
    Sleep(1000);
    Write_Log("log.log","The programme ends after ",clock()," ms.");
    return 0;
}

文件 log.log 中的内容:

[2024/10/19     19:50:29]:The programme begins.
[2024/10/19     19:50:30]:The programme ends after  1014  ms.

总结

这篇文章讲的只是关于可变参数模版最基础的只是,要熟练地运用,我们就需要多写代 (you) 码 (xi),这样才能加强对知识的了解。

参考资料:

  1. CSDN 【C++】C++11 可变参数模板(函数模板、类模板)作者:Yngz_Miao

  2. CSDN 【C++11】可变参数模版 /lambda 表达式 / 包装器 作者:KL4180

  3. CSDN 【C++11】可变参数模版 作者:jjrenhai

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

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

相关文章

Flink Lookup Join(维表 Join)

Lookup Join 定义&#xff08;支持 Batch\Streaming&#xff09; Lookup Join 其实就是维表 Join&#xff0c;比如拿离线数仓来说&#xff0c;常常会有用户画像&#xff0c;设备画像等数据&#xff0c;而对应到实时数仓场景中&#xff0c;这种实时获取外部缓存的 Join 就叫做维…

Azure Kubernetes Service (AKS)资源优化策略

针对Azure Kubernetes Service (AKS)的资源优化策略&#xff0c;可以从多个维度进行考虑和实施&#xff0c;以提升集群的性能、效率和资源利用率。以下是一些关键的优化策略&#xff1a; 一、 Pod资源请求和限制 设置Pod请求和限制&#xff1a;在YAML清单中为所有Pod设置CPU和…

RabbitMQ1:初识MQ

欢迎来到“雪碧聊技术”CSDN博客&#xff01; 在这里&#xff0c;您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者&#xff0c;还是具有一定经验的开发者&#xff0c;相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导&#xff0c;我将…

AI 提示词(Prompt)入门 十:最佳实践|详细询问,提供细节!

1、原则解释 当与 ChatGPT 交流时&#xff0c;提供具体和详细的信息非常重要。 这样做可以帮助 ChatGPT 更准确地理解你的需求和上下文&#xff0c;从而生成更相关和有用的回答 明确的信息可以包括具体的问题背景、相关领域的说明、你所期望的答案类型等。 2、如何实践 明…

实验十三 生态安全评价

1 背景及目的 生态安全是生态系统完整性和健康性的整体反映&#xff0c;完整健康的生态系统具有调节气候净化污染、涵养水源、保持水土、防风固沙、减轻灾害、保护生物多样性等功能。维护生态安全对于人类生产、生活、健康及可持续发展至关重要。随着城市化进程的不断推进&…

怎样实现跨部门和跨地区的数据共享?

随着企业规模的扩大和业务的多样化&#xff0c;不同部门和地区之间的数据共享变得越来越重要。实时数据同步作为保证数据准确性和完整性的重要手段&#xff0c;被广泛应用于各行各业。那不同部门和不同地区怎么实现共享数据呢&#xff1f; 一、前期数据准备 前期数据上需要建…

国家工信安全中心:公共数据授权运营平台技术要求(附下载)

2023年11月23日&#xff0c;第二届全球数字贸易博览会“数据要素治理与市场化论坛”于杭州成功召开&#xff0c;国家数据局党组书记、局长刘烈宏&#xff0c;浙江省委常委、常务副省长徐文光出席会议并致辞。会上&#xff0c;国家工业信息安全发展研究中心&#xff08;以下简称…

C语言数据结构——详细讲解 双链表

从单链表到双链表&#xff1a;数据结构的演进与优化 前言一、单链表回顾二、单链表的局限性三、什么是双链表四、双链表的优势1.双向遍历2.不带头双链表的用途3.带头双链表的用途 五、双链表的操作双链表的插入操作&#xff08;一&#xff09;双链表的尾插操作&#xff08;二&a…

【ArcGISPro】地理配准-影像校正

由于大部分数据安全性&#xff0c;以下是随意下载的图片&#xff0c;仅展示配置操作 地图-地理配准 添加控制点 修改控制点 可以导入、导出、添加和删除控制点 保存 关闭地理配准

ReNamer Pro 7.5 中文绿色便携专业版-文件重命名工具

前言 我们日常生活和工作中所涉及的文件数量日益增多。无论是图片、音频、视频还是各种文档&#xff0c;这些文件在存储、管理和分享时&#xff0c;都需要有一个清晰、有序的文件命名规则。然而&#xff0c;手动重命名大量文件不仅耗时耗力&#xff0c;而且容易出错&#xff0c…

PgSQL即时编译JIT | 第1期 | JIT初识

PgSQL即时编译JIT | 第1期 | JIT初识 JIT是Just-In-Time的缩写&#xff0c;也就是说程序在执行的时候生成可以执行的代码&#xff0c;然后执行它。在介绍JIT之前&#xff0c;需要说下两种执行方式&#xff1a;解释执行和编译执行。其中解释执行是通过解释器&#xff0c;将代码逐…

力扣-Hot100-数组【算法学习day.37】

前言 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非常非常高滴&am…

DataStream编程模型之数据源、数据转换、数据输出

Flink之DataStream数据源、数据转换、数据输出&#xff08;scala&#xff09; 0.前言–数据源 在进行数据转换之前&#xff0c;需要进行数据读取。 数据读取分为4大部分&#xff1a; &#xff08;1&#xff09;内置数据源&#xff1b; 又分为文件数据源&#xff1b; socket…

爬虫开发工具与环境搭建——使用Postman和浏览器开发者工具

第三节&#xff1a;使用Postman和浏览器开发者工具 在网络爬虫开发过程中&#xff0c;我们经常需要对HTTP请求进行测试、分析和调试。Postman和浏览器开发者工具&#xff08;特别是Network面板和Console面板&#xff09;是两种最常用的工具&#xff0c;能够帮助开发者有效地捕…

vue2侧边导航栏路由

<template><div><!-- :default-active"$route.path" 和index对应其路径 --><el-menu:default-active"active"class"el-menu-vertical-demo"background-color"#545c64"text-color"#fff"active-text-col…

时代变迁对传统机器人等方向课程的巨大撕裂

2020年之后&#xff0c;全面转型新质课程规划&#xff0c;传统课程规划全部转为经验。 农耕-代表性生产关系-封建分配制度主要生产力-人力工业-代表性生产关系-资本分配制度工业分为机械时代&#xff0c;电气时代&#xff0c;信息时代&#xff1b;主要生产力-人力转为人脑&…

JVM类加载过程-Loading

一、Class对象的生命周期 .class文件是如何加载到内存中:.class文件是ClassLoader通过IO将文件读到内存,再通过双亲委派的模式进行Loading,再Linking、以及Initializing,代码调用等一系列操作后,进行GC,组成完整的生命周期; 二、双亲委派模式(Loading的过程): 1、类…

BERT--公认的里程碑

前言 如果说&#xff0c;让我选Transformer架构的哪个模型最深入人心&#xff0c;我将毫不犹豫的选择BERT&#xff01; BERT 的意义在于&#xff0c;从大量无标记的数据集中训练得到的深度模型&#xff0c;可以限制提高各项自然语言处理任务的准确率。 BERT 在当时&#xff0…

<项目代码>YOLOv8 瞳孔识别<目标检测>

YOLOv8是一种单阶段&#xff08;one-stage&#xff09;检测算法&#xff0c;它将目标检测问题转化为一个回归问题&#xff0c;能够在一次前向传播过程中同时完成目标的分类和定位任务。相较于两阶段检测算法&#xff08;如Faster R-CNN&#xff09;&#xff0c;YOLOv8具有更高的…

每日OJ题_牛客_天使果冻_递推_C++_Java

目录 牛客_天使果冻_递推 题目解析 C代码 Java代码 牛客_天使果冻_递推 天使果冻 描述&#xff1a; 有 n 个果冻排成一排。第 i 个果冻的美味度是 ai。 天使非常喜欢吃果冻&#xff0c;但她想把最好吃的果冻留到最后收藏。天使想知道前 x个果冻中&#xff0c;美味…