C++可变参数模板

可变参数模板

一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。

可变数目的参数被称为参数包。

存在两种参数包:

  1. 模板参数类,表示零个或多个模板参数;
  2. 函数参数包,表示零个或多个函数参数。

我们用一个省略号来指出一个模板参数或函数参数表示一个包。

在一个模板参数列表中,class...或 typename...指出接下来的参数表示零个或多个类型的列表;

一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。

在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。例如:

//Args是一个模板参数包;rest是一个函数参数包
//Args表示零个或多个模板类型参数
//rest 表示零个或多个函数参数
template <typename T,typename...Args>
void foo(const T&t, const Args& ... rest);

声明了foo是一个可变参数函数模板,它有一个名为T的类型参数,和一个名为Args的模板参数包。这个包表示零个或多个额外的类型参数。

foo的函数参数列表包含一个const&类型的参数,指向T的类型,还包含一个名为rest的函数参数包,此包表示零个或多个函数参数。

与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。

例如,给定下面的调用:

int i = 0; double d = 3.14;
string s = "how now brown cow";

foo(i, s, 42, d); //包中有三个参数
foo(s,42, "hi"); // 包中有两个参数
foo (d, s); //包中有一个参数
foo("hi"); //空包

编译器会为foo实例化出四个不同的版本:

void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(constdouble&,const string&);
void foo(const char[3]&);

在每个实例中,T的类型都是从第一个实参的类型推断出来的。

剩下的实参(如果有的话)提供函数额外实参的数目和类型。

sizeof...运算符

当我们需要知道包中有多少元素时,可以使用 sizeof...运算符。

类似 sizeof,sizeof...也返回一个常量表达式,而且不会对其实参求值:

template<typename ... Args> 
void g(Args ... args) 
{
cout << sizeof...(Args) <<end1;//类型参数的数目
cout << sizeof...(args) << endl;// 函数参数的数目
)

 sizeof...是C++11中引入的可变参数的运算符,用于计算变参模板的参数数量。以下是使用sizeof...运算符的示例:

#include <iostream>

template<typename... Args>
void printArgsSize(Args... args) {
    std::cout << "Number of arguments: " << sizeof...(args) << std::endl;
}

int main() {
    printArgsSize(); // 输出:Number of arguments: 0
    printArgsSize(1, "hello", 3.14, true); // 输出:Number of arguments: 4

    return 0;
}

在这个例子中,我们定义了一个可变参数模板函数printArgsSize,它接受不定数量的参数,并使用sizeof...运算符来计算参数的数量。在main函数中,我们调用了printArgsSize函数两次,分别传入不同数量的参数,并输出参数的数量。

 编写可变参数函数模板

我们可以使用一个initializer_list 来定义一个 可接受可变数目实参的函数。

但是,所有实参必须具有相同的类型(或它们的类型可以转换为同一个公共类型)。

当我们既不知道想要处理的实参的数目也不知道它们的类型时可变参数函数是很有用的。

作为一个例子,我们将定义一个函数,它名为error_msg 函数,函数实参的类型是可变的。

我们首先定义一个名为print的函数,它在一个给定流上打印给定实参列表的内容。

可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。

我们的print函数也是这样的模式, 每次递归调用将第二个实参打印到第一个实参表示的流中。

为了终止递归,我们还需要定义一个非可变参数的print函数,它接受一个流和一个对象:

// 用来终止递归并打印最后一个元素的函数
//此函数必须在可变参数版本的print定义之前声明
template<typename T>
ostream& print(ostream& os, const T& t)
{
	return os << t;//包中最后一个元素之后不打印分隔符
}
//包中除了最后一个元素之外的其他元素都会调用这个版本的print
template <typename T, typename... Args>
ostream& print(ostream& os, const T& t, const Args&... rest)
{
	os << t << ","; // 打印第一个实参
	return print(os, rest...); // 递归调用,打印其他实参
}

第一个版本的print 负责终止递归并打印初始调用中的最后一个实参。

第二个版本的print是可变参数版本,它打印绑定到t的实参,并调用自身来打印函数参数包中的剩余值。

这段程序的关键部分是可变参数函数中对print的调用:

return print(os, rest...);//递归调用,打印其他实参

我们的可变参数版本的print函数接受三个参数:一个ostream&,·个const T&和一个参数包。

而此调用只传递了两个实参。其结果是rest中的第一个实参被绑定到t,剩余实参形成下一个print 调用的参数包。

因此,在每个调用中,包中的第一个实参被移除,成为绑定到t的实参。即,给定:

print(cout, i, s, 42);//包中有两个参数
调用trest...
print(cout,i,s,42)is,42
print(cout,s,42)s42
print(cout,42)调用非可变参数模板

前两个调用只能与可变参数版本的print匹配,非可变参数版本是不可行的,因为这两个调用分别传递四个和三个实参,而非可变参数print只接受两个实参。

对于最后一次递归调用print(cout,42),两个print版本都是可行的,这个调用传递两个实参,第一个实参的类型为ostream&,因此,可变参数版本的print可以实例化为只接受两个参数:一个是ostream&参数,另一个是const T&参数。

对于最后一个调用,两个函数提供同样好的匹配,但是,非可变参数模板比可变参数更特例化,
因此编译器选择非可变参数版本。


当定义可变参数版本的print时,非可变参数版本的声明必须在作用城中。否则,可变参数版本会无限递归

包扩展

对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展它。

当扩展一个包时,我们还要提供用于每个扩展元素的模式。

扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。

我们通过在模式右边放一个省略号(...)来触发扩展操作。

例如,我们的print函数包含两个扩展:

template < typename T, typename... Args>
// 扩展Args
ostream& print(ostream& os, const T& t, const Args&... rest)
{
	os << t << ",";
	return print(os, rest..); // 扩展 rest
}

第一个扩展操作扩展模板参数包,为print 生成函数参数列表。

第二个扩展操作出现在对print的调用中。此模式为print调用生成实参列表。

对Args的扩展中,编译器将模式const Arg&应用到模板参数包Args中的每个元素。

因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如const type&。

例如:

print (cout, i, 42);//包中有两个参数

最后两个实参的类型和模式一起确定了尾置参数的类型。此调用被实例化为:

ostream& print (ostreams, const int&, const string&, const int&);

第二个扩展发生在对print的(递归)调用中。

在此情况下,模式是函数参数包的名字(即rest)。此模式扩展出一个由包中元素组成的、逗号分隔的列表。

因此,这个调用等价于:

print(os, s, 42);

理解包扩展

print中的函数参数包扩展仅仅将包扩展为其构成元素,C++语言还允许更复杂的展模式。

例如,我们可以编写第二个可变参数函数,对其每个实参调用 debug_rep,然后调用print打印结果string:

//在print 调用中对每个实参调用debug_rep
template <typename... Args>
ostream& errorMsg (ostream &os, const Args&... rest)
{
// print (os, debug rep(al), debug rep(a2),.... debug_rep(an)
return print(os, debug_rep(rest)...);
}

这个print调用使用了模式debug reg(rest)。

此模式表示我们希望对函数参熟包rest中的每个元素调用 debug_rep。

扩展结果将是一个逗号分隔的debug_rep调用列表。

即,下面调用:

errorMsg(cerr, fcnName, code.num(), otherData, "other", item);

就好像我们这样编写代码一样

print(cerr, debug rep(fcnName), debug_rep(code.num()), 
                debug_rep(otherData),debug_rep("otherData"), 
                debug rep(item));


与之相对,下面的模式会编译失败

// 将包传递给 debug_rep; print (os, debug_rep(al, a2,..,an))
print(os,debug_rep(rest...));// 错误:此调用无匹配函数


这段代码的问题是我们在debug rep 调用中扩展了rest,它等价于

print (cerr, debug rep(fonName, code.num(),
                otherData, "otherData", item));

在这个扩展中,我们试图用一个五个实参的列表来调用debug_rep,但并不存在与此调用匹配的debuq_rep版本。debug_rep函数不是可变参数的,而且没有哪个debug_rep版本接受五个参数。

扩展中的模式会独立地应用于包中的每个元素。

转发参数包

在新标准下,我们可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。

作为例子,我们将为 StrVec类添加一个emplace_back成员。

标准库容器的 emplace_back成员是一个可变参数成员模板,它用其实参在容器管理的内存空间中直接构造个元素。

我们为Strvec 设计的emplace_back版本也应该是可变参数的,因为string有多个构造函数,参数各不相同。由于我们希望能使用string的移动构造函数,因此还需要保持传递给emplace_back的实参的所有类型信息。

如我们所见,保持类型信息是一个两阶段的过程。

首先,为了保持实参中的类型信息,必须将emplace_back的函数参数定义为模板类型参数的右值引用:

class StrVec
{
public:
template <class... Args> void emplace back (Args&&...);
//....
pair<string*, string*>  strvec::alloc_n_copy (const string *b, const string *e)
{
// 分配空间保存给定范围中的元素
auto data = alloc.allocate(e - b);
// 初始化并返回一个pair,该pair由data和uninitialized_copy的返回值构成
return {data, uninitialized_copy(b,e, data)};
}
};

模板参数包扩展中的模式是&&,意味着每个函数参数将是一个指向其对应实参的右值引用。

其次,当emplace back将这些实参传递给construct时,我们必须使用forward来保持实参的原始类型:

template <class... Args>
inline void StrVec::emplace_back (Args&&... args)
{
chk_n_alloc();// 如果需要的话重新分配StrVec 内存空间
alloc.construct(first free++, std::forward<Args>(args)...);
}


emplace_back的函数体调用了chk_n_alloc来确保有足够的空间容纳一个新元素,然后调用了construct在first_free指向的位置中创建了一个元素。

construct调用中的扩展为

std::forward<Args>(args)...


它既扩展了模板参数包Args,也扩展了函数参数包args。此模式生成如下形式的元素

std::forward<Ti>(ti)


其中T表示模板参数包中第1个元素的类型,t,表示函数参数包中第1个元素。例如,假定svec 是一个StrVec,如果我们调用

Svec.emplace_back(10,'c');// 将cceccccecc添加为新的尾元素

construct调用中的模式会扩展出

std::forward<int>(10), std::forward<char>(c)

通过在此调用中使用 forward,我们保证如果用一个右值调用emplace_back,则construct也会得到一个右值。

例如,在下面的调用中:

svec.emplace_back(s1 + s2);//使用移动构造函数

传递给emplace back的实参是一个右值,它将以如下形式传递给construct

std::forward<string>(string("the end"))

forward<string>的结果类型是string&&,因此construct将得到一个右值引用实参。

construct会继续将此实参传递给string的移动构造函数来创建新元素。

建议:转发和可变参数模板

可变参数函数通常将它们的参数转发给其他函数。这种函数通常具有与我们的emplace back函数一样的形式:

// fun有零个或多个参数,每个参数都是一个模板参数类型的右值引用
template<typename... Args>
void fun(Args&&... args) //将Args扩展为一个右值引用的列表
{
// work 的实参既扩展 Args又扩展 args
work(std::forward<Args>(args)...);
}


这里我们希望将fun的所有实参转发给另一个名为work的函数,假定由它完成函数的实际工作。类似emplace back中对construct的调用,work调用中的扩展既扩展了模板参数包也扩展了函数参数包。

由于 fun 的参数是右值引用,因此我们可以传递给它任意类型的实参;由于我们使用std::forward传递这些实参,因此它们的所有类型信息在调用work时都会得到保持。
 

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

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

相关文章

雷弗流体创新技术装备与您与您相约2024第13届生物发酵展

参展企业介绍 保定雷弗流体科技有限公司于2010年1月成立。为创新型企业&#xff0c;荣获国家级高新技术企业、国家级专精特新小巨人企业、河北省单项冠军企业、组织部巨人计划创业团队等荣誉称号。 保定雷弗流体科技有限公司现有职工180人&#xff0c;其中工程技术人员53人。现…

GitHub入门与实践

ISBN: 978-7-115-39409-5 作者&#xff1a;【日】大塚弘记 译者&#xff1a;支鹏浩、刘斌 页数&#xff1a;255页 阅读时间&#xff1a;2023-08-05 推荐指数&#xff1a;★★★★★ 好久之前读完的了&#xff0c;一直没有写笔记。 这本入门Git的书籍还是非常推荐的&#xff0c;…

【服务器部署篇】Linux下安装Docker容器

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过大学刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0…

X服务器远程连接问题解决:Bad displayname ““‘或Missing X server or $DISPLAY

X服务器远程连接问题 报错1 ImportError: this platform is not supported: (failed to acquire X connection: Bad displayname "", DisplayNameError()) Try one of the following resolutions: * Please make surethat you have an X server running, and that …

基于无线物联网的智能配电监控系统设计应用

摘要&#xff1a;阐述基于电力物联网的智能配电监控系统的特点&#xff0c;探讨物联网结构及其关键技术&#xff0c;电力物联网下的智能配电监控系统设计&#xff0c;包括整体结构设计、硬件和软件系统设计。 安科瑞薛瑶瑶18701709087 关键词&#xff1a;电力物联网&#xff…

AI水下颜色校正解决方案,助力企业打造水下视觉盛宴

水下摄影作为一种独特且富有挑战性的拍摄方式&#xff0c;正受到越来越多旅行者和摄影师的青睐。然而由于海水的光线折射和金属成分的影响&#xff0c;水下拍摄的照片和视频往往存在严重的偏色问题&#xff0c;无法真实还原水下世界的美丽与神奇。美摄科技凭借深厚的技术积累和…

【动态规划】【01背包】Leetcode 416. 分割等和子集

【动态规划】【01背包】Leetcode 416. 分割等和子集 ---------------&#x1f388;&#x1f388;416. 分割等和子集 题目链接&#x1f388;&#x1f388;------------------- 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0…

顺序表的应用之通讯录

学习了顺序表之后&#xff0c;我们也得知道它的实际用途吧&#xff01;所以&#xff0c;我们今天来学习一下通讯录的实现。 typedef struct personInfo SLDataType; contact.h #define NAME_MAX 20 #define GENDER_MAX 20 #define GTEL_MAX 20 #define ADDR_MAX 100 #include&…

芯课堂 | JScope虚拟示波器使用说明

​1. 首先需要安装Jlink的驱动&#xff0c;即安装JLink_Windows_V634e之后才能安装JScope&#xff0c;一般这个能正常使用Jlink下载、仿真说明你的Jlink驱动已经正常安装 2. 需要安装Jscope&#xff0c;即安装Setup_JScope_V611m&#xff0c;安装完成之后能看到以下画面 3. 新建…

ip地址电脑哪里看?一文揭秘

在数字化和网络化的今天&#xff0c;IP地址对于电脑用户而言具有至关重要的意义。无论是进行网络配置、故障排除还是安全管理&#xff0c;了解如何查看电脑的IP地址都是一项必备技能。虎观代理将深入解析IP地址的概念&#xff0c;详细指导用户如何在电脑上查看IP地址&#xff0…

红黑树插入机制深度剖析与实践指南

红黑树插入机制深度剖析与实践指南 一、红黑树的基本概念二、插入操作的初步2.1 RB-INSERT-FIXUP过程2.2 循环的不变性2.2.1 情况1&#xff1a;叔节点是红色2.2.2情况2和情况3&#xff1a;叔节点是黑色 三、插入操作的复杂性分析四、伪代码4.1 RB-INSERT 过程4.2 RB-INSERT-FIX…

angular—mooc课学习笔记

1.angular工程目录 2.设置标签元素样式 3.fex布局 4.事件绑定 5. 双向数据传输 6. 键盘实现方法

新生儿斜视:早期发现与关爱的重要性

引言&#xff1a; 新生儿斜视是一种常见的眼睛问题&#xff0c;如果不及时发现和治疗&#xff0c;可能会影响宝宝的视觉发展。因此&#xff0c;家长们需要重视并及时关注宝宝眼睛的情况&#xff0c;以便及早发现并处理斜视问题。在本文中&#xff0c;我们将探讨新生儿斜视的注意…

蓝桥杯刷题 前缀和与差分-[NewOJ P1819]推箱子(C++)

题目描述 在一个高度为H的箱子前方&#xff0c;有一个长和高为N的障碍物。 障碍物的每一列存在一个连续的缺口&#xff0c;第i列的缺口从第l各单位到第h个单位&#xff08;从底部由0开始数&#xff09;。 现在请你清理出一条高度为H的通道&#xff0c;使得箱子可以直接推出去。…

蓝桥杯刷题-09-三国游戏-贪心⭐⭐⭐

蓝桥杯2023年第十四届省赛真题-三国游戏 小蓝正在玩一款游戏。游戏中魏蜀吴三个国家各自拥有一定数量的士兵X, Y, Z (一开始可以认为都为 0 )。游戏有 n 个可能会发生的事件&#xff0c;每个事件之间相互独立且最多只会发生一次&#xff0c;当第 i 个事件发生时会分别让 X, Y,…

GitHub突破1000星!上交、清华开源个性化联邦学习算法库PFLlib

©PaperWeekly 原创 作者 | 张剑清 单位 | 上海交通大学、清华大学&#xff08;AIR&#xff09; 研究方向 | 联邦学习 我们在 GitHub 上开源了一个个性化联邦学习算法仓库&#xff08;PFLlib&#xff09;&#xff0c;目前已经获得 1K 个 Star 和 200 个 Fork&#xff0c;在…

【C++】探索C++中的类与对象(下)---深入理解C++中的关键概念与应用

​​ &#x1f331;博客主页&#xff1a;青竹雾色间. &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞⭐收藏➕关注 ✨人生如寄&#xff0c;多忧何为 ✨ 在C编程中&#xff0c;有许多重要的概念和特性&#xff0c;包括构造函数、explicit关键字、静态成员、友元以及内部类…

58 vue-cli 以及 webpack 提供的默认的插件, 配置

前言 vue-cli 这边作为驱动 webpack 的一个应用 它需要构造 webpack 所需要的上下文, 以及参数 这里 我们来关注一下 vue-cli 这边为 webpack 构造的参数 的相关处理 webpack 这边上下文的配置, 主要分为了几个部分, Entry, Output, Module, Resolve, Plugin, DevServer, O…

Linux下Qt生成程序崩溃文件

文章目录 1.背景2.Qt编译生成程序2.1.profile模式的本质 3.执行程序&#xff0c;得到core文件4.代码定位4.1.直接使用gdb4.2.使用QtCreator 5.总结6.题外话6.1.profile模式和debug模式的区别 1.背景 在使用Qt时&#xff0c;假如在windows&#xff0c;当软件崩溃时&#xff0c;…

太阳能光伏电子实验酸洗用PFA方槽耐受强酸碱耐高温

PFA清洗槽是四氟清洗桶后的升级款&#xff0c;主要用于半导体光伏光电等行业&#xff0c;一体成型&#xff0c;无需担心漏液&#xff0c;表面光滑无毛刺。 别名PFA浸泡桶、PFA酸缸、PFA方槽等&#xff0c;可定制尺寸&#xff0c;可配套盖子&#xff0c;盖子有PFA/PTFE两种材质…