可变参数模板

可变参数模板的概念

可变参数模板是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。

在C++98/03中,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。

可变参数模板的定义方式

函数的可变参数模板定义方式如下:

template<class …Args>
返回类型 函数名(Args… args)
{
  //函数体
}

我们来看一个具体实例:

template<class ...Args>
void ShowList(Args... args)
{}

分析:模板参数Args前面有省略号,代表它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面可以包含0到N ( N ≥ 0 )个模板参数,而args则是一个函数形参参数包。

例如我们调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。

int main()
{
	ShowList();
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', string("hello"));
	return 0;
}

我们可以在函数模板中通过sizeof计算参数包中参数的个数。

template<class ...Args>
void ShowList(Args... args)
{
	cout << sizeof...(args) << endl; //获取参数包中参数的个数
}

但是我们需要注意的是语法上并不支持使用args[i]的方式来获取参数包中的参数。我们只能通过展开参数包的方式来获取。

template<class ...Args>
void ShowList(Args... args)
{
	//错误示例:
	for (int i = 0; i < sizeof...(args); i++)
	{
		cout << args[i] << " "; //打印参数包中的每个参数
	}
	cout << endl;
}

参数包的展开方式

递归展开参数包

递归展开参数包的方式如下:

  • 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来。
  • 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
  • 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来。

比如我们要打印调用函数时传入的各个参数,那么函数模板可以这样编写:

//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印分离出的第一个参数
	ShowList(args...);    //递归调用,将参数包继续向下传
}

为了能够结束函数的递归调用,我们再编写一个无参的递归终止函数,这样当传入的参数包中参数个数为0,那么就会匹配到无参的递归终止函数,从而结束了递归。

//递归终止函数
void ShowList()
{
	cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印分离出的第一个参数
	ShowList(args...);    //递归调用,将参数包继续向下传
}

我们来看一个特殊场景:当外部调用ShowList函数时,如果我们没有传入参数的话,例如“ ShowList(); ”我们经过调试后会发现它直接匹配到无参的递归终止函数。而我们本意是想让外部调用ShowList函数时匹配的都是函数模板,而不是让外部调用时直接匹配到这个递归终止函数。因此我们可以将展开函数和递归调用函数的函数名改为ShowListArg,然后重新编写一个ShowList函数模板,这样我们无论有没有传入参数都会先匹配函数模板,然后函数模板根据参数的个数来调用相应的ShowListArg函数展开参数包(个数为0调用递归终止函数,个数大于0调用展开函数)。

//递归终止函数
void ShowListArg()
{
	cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowListArg(T value, Args... args)
{
	cout << value << " "; //打印传入的若干参数中的第一个参数
	ShowListArg(args...); //将剩下参数继续向下传
}

template<class ...Args>
void ShowList(Args... args)
{
	ShowListArg(args...);
}

当然我们也可以编写带参的递归终止函数来规避这个问题。

// 递归终止函数
template <class T>
void ShowList(const T& t)
{
    cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
    cout << value <<" ";
    ShowList(args...);
}
int main()
{
    ShowList();  //编译报错
    ShowList(1);
    ShowList(1, 'A');
    return 0;
}

我们在调用ShowList函数时必须至少传入一个参数,否则就会报错。因为展开函数和递归终止函数都至少需要传入一个参数。

逗号表达式展开参数包

我们先来看一个场景:如果参数包中各个参数的类型都是整型,那么我们可以把这个参数包放到列表当中初始化这个整型数组,此时参数包中参数就放到数组中了。

//展开函数
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { args... }; //列表初始化
	//打印参数包中的各个参数
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;
}

这时调用ShowList函数时就可以传入多个整型参数

int main()
{
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	return 0;
}

但是C++规定一个容器中存储的数据类型必须是相同的,因此如果这样写的话,那么调用ShowList函数时传入的参数只能是整型的,因此我们为了能够传入不同的类型,还需要在此基础上借助逗号表达式来展开参数包。

虽然我们不能用不同类型的参数去初始化一个整型数组,但我们可以借助逗号表达式。

  • 逗号表达式会从左到右依次计算各个表达式,并且将最后一个表达式的值作为返回值进行返回。
  • 将逗号表达式的最后一个表达式设置为一个整型值,确保逗号表达式返回的是一个整型值。
  • 将处理参数包中参数的过程封装成一个函数,将该函数的调用作为逗号表达式的第一个表达式。

这样一来,在执行逗号表达式时就会先调用处理函数处理对应的参数,然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。

//处理参数包中的每个参数
template<class T>
void PrintArg(const T& t)
{
	cout << t << " ";
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
	cout << endl;
}

注意:可变参数的省略号需要加在逗号表达式外面,表示需要将逗号表达式展开。代码中的{(PrintArg(args), 0)...}将会展开成{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc...}

这时调用ShowList函数时就可以传入多个不同类型的参数了。

STL容器中的emplace相关接口函数

C++11给STL中的容器增加emplace版本的插入接口,比如list容器的push_front、push_back和insert函数,都增加了对应的emplace_front、emplace_back和emplace函数。

这些emplace版本的插入接口支持模板的可变参数,比如list容器的emplace_back函数的声明如下:

注意: emplace系列接口的可变模板参数类型都带有“&&”,这个表示的是万能引用,而不是右值引用。

使用方法 

emplace系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。

以list容器的emplace_back和push_back为例:

  • 调用push_back函数插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
  • 调用emplace_back函数插入元素时,也可以传入左值对象或者右值对象,但不可以使用列表进行初始化。

除此之外,emplace系列接口最大的特点就是,插入元素时可以传入用于构造元素的参数包

int main()
{
	list<pair<int, string>> lt;
	pair<int, string> kv(10, "111");
	lt.push_back(kv);                              //传左值
	lt.push_back(pair<int, string>(20, "222"));    //传右值
	lt.push_back({ 30, "333" });                   //列表初始化

	lt.emplace_back(kv);                           //传左值
	lt.emplace_back(pair<int, string>(40, "444")); //传右值
	lt.emplace_back(50, "555");                    //传参数包
	return 0;
}

工作流程

  1. 先通过空间配置器为新结点获取一块内存空间,注意这里只会开辟空间,不会自动调用构造函数对这块空间进行初始化。
  2. 然后调用allocator_traits::construct函数对这块空间进行初始化,调用该函数时会传入这块空间的地址和用户传入的参数(需要经过完美转发)。
  3. 在allocator_traits::construct函数中会使用定位new表达式,显示调用构造函数对这块空间进行初始化,调用构造函数时会传入用户传入的参数(需要经过完美转发)。
  4. 将初始化好的新结点插入到对应的数据结构当中,比如list容器就是将新结点插入到底层的双链表中。

意义

由于emplace系列接口的可变模板参数的类型都是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。

  • 如果调用emplace系列接口时传入的是左值对象,那么首先需要先在此之前调用构造函数实例化出一个左值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数。
  • 如果调用emplace系列接口时传入的是右值对象,那么就需要在此之前调用构造函数实例化出一个右值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,就会匹配到移动构造函数。
  • 如果调用emplace系列接口时传入的是参数包,那就可以直接调用函数进行插入,并且最终在使用定位new表达式调用构造函数对空间进行初始化时,匹配到的是构造函数。

总结一下:

  • 传入左值对象,需要调用构造函数+拷贝构造函数。
  • 传入右值对象,需要调用构造函数+移动构造函数。
  • 传入参数包,只需要调用构造函数。

emplace接口的意义:

  • emplace系列接口最大的特点就是支持传入参数包用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因。
  • 但emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的push_back插入接口的效率是一样的。
  • emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了额外的一次拷贝。

验证

我们先来编写一个简化版的string。

namespace a
{
	class string
	{
	public:
		//构造函数
		string(const char* str = "")
		{
			cout << "string(const char* str) -- 构造函数" << endl;

			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		//交换两个对象的数据
		void swap(string& s)
		{
			//调用库里的swap
			::swap(_str, s._str); 
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		//拷贝构造函数(现代写法)
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl;

			string tmp(s._str);
			swap(tmp);
		}
		//移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}
		//拷贝赋值函数(现代写法)
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;

			string tmp(s);
			swap(tmp);
			return *this;
		}
		//移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
		//析构函数
		~string()
		{
			//delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

我们在string的构造函数、拷贝构造函数和移动构造函数当中均打印了一条提示语句,因此我们可以通过控制台输出来判断这些函数是否被调用。

下面我们用一个容器来存储模拟实现的string,并以不同的传参形式调用emplace系列函数。

int main()
{
	list<pair<int, a::string>> lt;
	
	pair<int, a::string> kv(1, "one");
	lt.emplace_back(kv);                              //传左值
	cout << endl;
	lt.emplace_back(pair<int, a::string>(2, "two")); //传右值
	cout << endl;
	lt.emplace_back(3, "three");                      //传参数包
	return 0;
}

注意:我们在实现string的拷贝构造函数中复用了构造函数,因此在传左值的时候拷贝构造后面会再调用一次构造函数。

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

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

相关文章

MicroPython 树莓派 RP2 入门教程

系列文章目录 前言 Raspberry Pi Pico 开发板&#xff08;图片来源&#xff1a;Raspberry Pi 基金会&#xff09;。 以下是 Raspberry Pi RP2xxx 板的快速参考资料。如果您是第一次使用该开发板&#xff0c;了解微控制器的概况可能会对您有所帮助&#xff1a; 一、关于 RP2xxx…

MPLS-基础、LSR、LSP、标签、体系结构

MPLS技术 MPLS基础 MPLS&#xff1a;转发数据时&#xff0c;只在网络边缘分析IP报文头&#xff0c;不在每一跳都分析&#xff0c;节约了转发时间。 MPLS&#xff1a;Multiprotocol Label Switching&#xff0c;多协议标签交换骨干网技术。主要应用&#xff1a;VPN、流量工程…

Day5-Hive的结构和优化、数据文件存储格式

Hive 窗口函数 案例 需求&#xff1a;连续三天登陆的用户数据 步骤&#xff1a; -- 建表 create table logins (username string,log_date string ) row format delimited fields terminated by ; -- 加载数据 load data local inpath /opt/hive_data/login into table log…

小程序如何设置余额充值和消费功能

小程序中设置余额充值和消费功能非常重要的&#xff0c;通过让客户在小程序中进行余额充值&#xff0c;不仅可以提高用户粘性&#xff0c;还可以促进消费&#xff0c;增加用户忠诚度。以下是如何在小程序中设置余额充值和消费功能的步骤&#xff1a; 1. **设计充值入口**&…

Web3 革命:揭示区块链技术的全新应用

随着数字化时代的不断发展&#xff0c;区块链技术作为一项颠覆性的创新正在改变着我们的世界。而在这一技术的进步中&#xff0c;Web3正逐渐崭露头角&#xff0c;为区块链技术的应用带来了全新的可能性。本文将探讨Web3革命所揭示的区块链技术全新应用&#xff0c;并展望其未来…

【Redis】Redis群集的三种模式(主从、哨兵、群集)

redis群集有三种模式&#xff0c;分别是主从同步/复制、哨兵模式、Cluster&#xff0c;下面会讲解一下三种模式的工作方式&#xff0c;以及如何搭建cluster群集 ●主从复制&#xff1a;主从复制是高可用Redis的基础&#xff0c;哨兵和集群都是在主从复制基础上实现高可用的。主…

【性能优化】查询性能优化

目录 1. 精简字段给前端2. SQL优化3. 多级缓存4. IO连接优化之外考虑计算操作优化5. 请求层配置6. 使用更快的框架 服务器推荐 2核4g 1核2g 2核2g 的服务器&#xff0c;4000 - 5000 qps 就非常不错了。 1. 精简字段给前端 字段信息太多会影响下载内容的速度 2. SQL优化 例如…

基于springboot的学校访客登记系统

基于SpringBoot的学校访客登记系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatis工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 功能需求 本学校访客登记系统通过信息化的技术实现了访客管理流程的信息化的管理操作。平…

iOS开发之Swift标识符

iOS开发之Swift标识符 在iOS开发中&#xff0c;使用Swift语言时&#xff0c;标识符是用来命名变量、常量、函数、类、结构体、枚举等程序实体的&#xff1b; 这些标识符使得Swift代码更加清晰、易于理解和维护。 一、变量与常量&#xff1a;var、let var代表variable&#…

C++:函数重载和引用

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习C&#xff1a;函数重载和引用&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 文章目录 函数重载1.函数重载的概念为什么C支持函数重载 引用引用的概念引…

基于SpringBoot+Vue的儿童书法机构管理系统

一、项目背景介绍&#xff1a; 儿童书法机构的管理是一个具有挑战性的任务&#xff0c;需要有效的信息管理和资源分配。传统的手动管理方式存在着效率低下、易出错等问题。为了解决这些问题&#xff0c;基于SpringBootVue的儿童书法机构管理系统应运而生。该系统利用现代化的技…

代码随想录第三十一天 | 贪心算法P1 | ● 理论基础 ● 455. ● 376. ● 53.

理论基础 题目分类 一般解题步骤 贪心算法一般分为如下四步&#xff1a; 将问题分解为若干个子问题找出适合的贪心策略求解每一个子问题的最优解将局部最优解堆叠成全局最优解 这个四步其实过于理论化了&#xff0c;我们平时在做贪心类的题目 很难去按照这四步去思考&…

数据结构:顺序表的基本操作!(C语言)

一、静态存储 #include <stdio.h> #include <stdlib.h>/*[1].定义静态顺序表的最大容量*/ #define MaxSize 10/*[2].自定义数据元素的数据类型*/ typedef int ElemType; 1. 静态分配的定义结构体 /*[3].静态分配的结构体定义*/ typedef struct {ElemType data[M…

最新AI创作系统ChatGPT网站系统源码+Ai绘画网站源码+Suno-v3-AI音乐生成大模型(sparkAi系统V6版本)

一、前言 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧。已支持GPT…

rt-thread的nfs如何实现软硬件对接

rt-thread&#xff08;下面简称rtt&#xff09;有一个封装好的的虚拟文件系统&#xff0c;提供了一套通用的io文件接口例如 open,write,read这些&#xff0c;从没看过rtt的代码也没用过&#xff0c;文件系统在刚毕业的时候只是用过fatfs但没去纠结过。今年1月份听同事说只需要打…

C++(语法以及易错点2)

1.内联函数 1.1 概念 以inline修饰的函数叫做内联函数&#xff0c;编译时C编译器会在调用内联函数的地方展开&#xff0c;没有函数调 用建立栈帧的开销&#xff0c;内联函数提升程序运行的效率。 ​int ADD(int a,int b) {return ab; }​ 1.2 特性 1. inline是一种以空间换时间…

Java数据结构栈

栈&#xff08;Stack&#xff09; 概念 栈是一种先进后出的数据结构。 栈的使用 import java.util.Stack; public class Test {public static void main(String[] args) {Stack<Integer> s new Stack();s.push(1);s.push(2);s.push(3);s.push(4);System.out.println(s…

精密电阻阻值表和电容容值表

前面2张是电阻阻值表&#xff08;E-96/0603/1%&#xff09; 常见贴片电容的容值表

【智能优化算法】非洲秃鹫优化算法:一种新的全局优化问题的自然启发的元启发式算法

非洲秃鹫优化算法&#xff08;AVOA&#xff09;发表在中科院一区Computers & Industrial Engineering期刊上的论文“African vultures optimization algorithm: A new nature-inspired metaheuristic algorithm for global optimization problems" 01.引言 元启发式算…

DAY16|104.二叉树的最大深度,111.二叉树的最小深度,222完全二叉树的个数

文章目录 104.二叉树的最大深度111.二叉树的最小深度222.完全二叉树的个数 104.二叉树的最大深度 文字讲解&#xff1a;二叉树的层序遍历 视频讲解&#xff1a;二叉树的层序遍历 状态&#xff1a;求深度用前序遍历&#xff0c;求高度用后序遍历&#xff1b; 思路&#xff1a; …