C++11之可变参数模板

news2024/12/23 13:32:59

可变参数模板

  • 可变参数模板概念
  • 可变参数模板定义
  • 参数包展开方式
    • 递归展开参数包
    • 逗号表达式展开参数包
  • STL容器中的emplace相关接口函数

可变参数模板概念

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

可变参数模板定义

下面就是一个基本可变参数的函数模板:其中,Args是一个模板参数包,args是一个函数形参参数包
声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。

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

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

接下来我们就可以调用ShowList函数,它的参数可以是任意参数类型:

int main()
{
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 'A');
	ShowList(1, 2, '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 < args; i++)
	{
		//错误展开方式
		cout << args[i] << endl;
	}
}

参数包展开方式

递归展开参数包

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

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

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

template <class T, class ...Args>
void ShowList(T val, Args... args)
{
	cout << val << endl;//打印分离出来的第一个参数
	ShowList(args...);//递归调用,将参数继续往下传
}

我们还需在刚才的基础上,再编写一个无参的递归终止函数,该函数的函数名与展开函数的函数名相同:

//递归终止函数
void ShowList()
{
	cout << endl;
}

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

当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。

  • 但如果外部调用ShowList函数时就没有传入参数,那么就会直接匹配到无参的递归终止函数。
  • 而我们本意是想让外部调用ShowList函数时匹配的都是函数模板,并不是让外部调用时直接匹配到这个递归终止函数。

我们可以将展开函数和递归调用函数的函数名改为__ShowList,然后重新编写一个ShowList函数模板,该函数模板的函数体中要做的就是调用__ShowList函数展开参数包:

//递归终止函数
void __ShowList()
{
	cout << endl;
}

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

//供外部调用的函数
template <class ...Args>
void ShowList(Args... args)
{
	__ShowList(args...);
}

我们除了编写无参的递归终止函数,也可以编写带参数的递归终止函数来终止递归:

//递归终止函数
template<class T>
void __ShowList(const T& t)
{
	cout << t << endl;
}

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

//供外部调用的函数
template <class ...Args>
void ShowList(Args... args)
{
	__ShowList(args...);
}

这样一来,在递归调用过程中,如果传入的参数包中参数的个数为1,那么就会匹配到这个递归终止函数,这样也就结束了递归。但是需要注意,这里的递归调用函数需要写成函数模板,因为我们并不知道最后一个参数是什么类型的。该方法还有一个弊端就是,我们在调用ShowList函数时必须至少传入一个参数,否则就会报错。因为此时无论是调用递归终止函数还是展开函数,都需要至少传入一个参数。

逗号表达式展开参数包

通过列表获取参数包中的参数

我们所学习的数组就是可以通过列表进行初始化的:

int arr[] = {1, 2, 3, 4};

如果我们的参数包中的每个元素都是整形,我们就可以将这个参数包放到列表当中初始化这个整型数组,此时参数包中参数就放到数组中了:

template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { args... };

	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;
}

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

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

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

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

C++11标准给STL中的容器增加emplace版本的插入接口,我们以list为例:
在这里插入图片描述
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和emplace系列接口的优势到底在哪里呢?

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

以list容器的emplace_back和push_back为例:

  • 调用push_back函数插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
  • 调用emplace_back函数插入元素时,也可以传入左值对象或者右值对象,但不可以使用列表进行初始化。
  • 除此之外,emplace系列接口最大的特点就是,插入元素时可以传入用于构造元素的参数包。
int main()
{
	list<pair<int, string>> mylist;
	pair<int, string> kv(10, "111");
	mylist.push_back(kv);                              //传左值
	mylist.push_back(pair<int, string>(20, "222"));    //传右值
	mylist.push_back({ 30, "333" });                   //列表初始化

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

emplace系列接口的工作流程

emplace系列接口的工作流程如下:

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

emplace系列接口的意义

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

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

整体来说,emplace只有在传入参数包的过程中会很好的提高效率,对于左值引用和右值引用跟insert相比并没有什么太大的区别。

验证:

namespace gtt
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动语义" << endl;
			swap(s);
		}
		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动语义" << endl;
			swap(s);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

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

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

运行叫我们就会发现,传参数包时就会少调用一次构造函数:
在这里插入图片描述

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

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

相关文章

Java进阶02 Array、内存分析、this、面向对象、继承、override、super、实例化、多态、向下转型、Object

文章目录 一、数组(Array)二、数组的内存分析三、Array工具类四、面向对象的一些小知识五、进阶知识补充1. this关键字2.继承3.方法重写4.super关键字的使用5.子类对象实例化6.多态性的体现7.向下转型8.Object类 一、数组(Array) 数组&#xff1a;多个相同类型数据按照一定顺序…

基于 SpringBoot 2.7.x 使用最新的 Elasticsearch Java API Client 之 ElasticsearchClient

1. 从 RestHighLevelClient 到 ElasticsearchClient 从 Java Rest Client 7.15.0 版本开始&#xff0c;Elasticsearch 官方决定将 RestHighLevelClient 标记为废弃的&#xff0c;并推荐使用新的 Java API Client&#xff0c;即 ElasticsearchClient. 为什么要将 RestHighLevelC…

大喜国庆,聊聊我正式进入职场的这三个月...

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

Thymeleaf 内联语法使用教程

1 表达式内联 Thrmeleaf标准方言允许使用标签属性(th:)来实现很多的功能&#xff0c;但在有些场景之下&#xff0c;需要将表达式直接写入我们HTML 代码中和CSS代码中及JavaScript代码中【代码和html文件在一起&#xff0c;分能不开&#xff0c;待验证】&#xff0c;称为内联…

[Unity][VR]Oculus透视开发图文教程1-Passthrough应用XR项目设置

Oculus现在已向开发者公布了如何使用自己的设备Camera,本系列课程就来手把手地告诉你如何在Unity中使用这个特性。 第一步,既然用的是Quest的特性,那就需要先引入Quest的Unity开发SDK。并且完成基本的VR开发项目设置。 新建Unity项目后,在编辑器界面先点击Window,打开资…

【实验记录】一些小疑问

1.为什么要选择基于“外观”这一特性来作为回环检测的方案&#xff1f; 朴素思路复杂度高&#xff0c;不利于实时性&#xff1b;基于“里程计”的方案需要知道相机处于何位置下才能发生检测&#xff0c;这与我们需要知道的准确位置相矛盾 基于“外观”的方案与前端和后端均无关…

计算机图形学、贝塞尔曲线及绘制方法、反走样问题的解决(附完整代码)

贝塞尔曲线 1. 本次作业实现的函数及简单描述&#xff08;详细代码见后&#xff09;2. 与本次作业有关的基础知识整理3. 代码描述&#xff08;详细&#xff09;4. 完整代码5. 参考文献 &#xff08;本篇为作者学习计算机图形学时根据作业所撰写的笔记&#xff0c; 如有同课程请…

进程的状态与转换以及组织方式

1.进程的状态 三种基本状态&#xff1a;运行态&#xff0c;就绪态&#xff0c;阻塞态。 1.运行状态 如果一个进程此时在CPU上运行&#xff0c;那么这个进程处于“运行态”。 CPU会执行该进程对应的程序&#xff08;执行指令序列) 2.就绪状态 当进程创建完成后&#xff0c;…

【论文阅读】(CVPR2023)用于半监督医学图像分割的双向复制粘贴

目录 前言方法BCPMean-teacher and Traning StrategyPre-Training via Copy-PasteBidirectional Copy-Paste ImagesBidirectional Copy-Paste Supervisory Signals Loss FunctionTesting Phase 结论 先看这个图&#xff0c;感觉比较清晰。它整个的思路就是把有标签的图片和无标…

动态规划算法(1)--矩阵连乘

目录 一、动态数组 1、创建动态数组 2、添加元素 3、删除修改元素 4、访问元素 5、返回数组长度 6、for each遍历数组 二、输入多个数字 1、正则表达式 2、has.next()方法 三、矩阵连乘 1、什么是矩阵连乘&#xff1f; 2、动态规划思路 3、手推m和s矩阵 4、完…

AI伦理与机器道德:人工智能的道德挑战

文章目录 什么是AI伦理和机器道德&#xff1f;1. 隐私保护2. 歧视和不平等3. 透明度和解释性4. 安全性5. 社会影响 AI伦理和机器道德的重要性1. 保护个人权利2. 避免不平等和歧视3. 保持透明和责任4. 促进创新 AI伦理挑战和解决方案1. 隐私保护2. 歧视和不平等3. 透明度和解释性…

P1541 [NOIP2010 提高组] 乌龟棋(4维背包问题)

[NOIP2010 提高组] 乌龟棋 题目背景 小明过生日的时候&#xff0c;爸爸送给他一副乌龟棋当作礼物。 题目描述 乌龟棋的棋盘是一行 N N N 个格子&#xff0c;每个格子上一个分数&#xff08;非负整数&#xff09;。棋盘第 1 1 1 格是唯一的起点&#xff0c;第 N N N 格是…

Nginx简介与Docker Compose部署指南

Nginx是一款高性能的开源Web服务器和反向代理服务器&#xff0c;以其卓越的性能、可伸缩性和灵活性而闻名。它在全球范围内广泛用于托管Web应用程序、负载均衡、反向代理和更多场景中。在本文中&#xff0c;我们将首先介绍Nginx的基本概念&#xff0c;然后演示如何使用Docker C…

Apollo自动驾驶系统概述(文末参与活动赠送百度周边)

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

数据结构之美:如何优化搜索和排序算法

文章目录 搜索算法的优化1. 二分搜索2. 哈希表 排序算法的优化1. 快速排序2. 归并排序 总结 &#x1f389;欢迎来到数据结构学习专栏~数据结构之美&#xff1a;如何优化搜索和排序算法 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#xff1a;IT陈寒的博客&#x…

阿里云ECS服务器无法发送邮件问题解决方案

这篇文章分享一下自己把项目部署在阿里云ECS上之后&#xff0c;登录邮件提醒时的邮件发送失败问题&#xff0c;无法连接发送邮箱的服务器。 博主使用的springboot提供的发送邮件服务&#xff0c;如下所示&#xff0c;为了实现异步的效果&#xff0c;新开了一个线程来发送邮件。…

【Vim 插件管理器】Vim-plug和Vim-vbundle的区别

- vundle是一款老款的插件管理工具 - vim-plug相对较新&#xff0c;特点是支持异步加载&#xff0c;相比vundle而言 Vim-plug 是一个自由、开源、速度非常快的、极简的 vim 插件管理器。它可以并行地安装或更新插件。你还可以回滚更新。它创建浅层克隆shallow clone最小化磁盘…

【数据结构初阶】六、线性表中的队列(C语言 -- 链式结构实现队列)

相关代码gitee自取&#xff1a; C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 【数据结构初阶】五、线性表中的栈&#xff08;C语言 -- 顺序表实现栈&#xff09;_高高的胖子的博客-CSDN博客 1 . 队列&#xff08;Queue&#xff09; 队列的概念和结构&#xf…

如何利用niceGUI构建一个流式单轮对话界面

官方文档 参考文档 import asyncio import time import requests from fastapi import FastAPI from nicegui import app, uiclass ChatPage:temperature: ui.slider Nonetop_p: ui.slider Noneapi_key: ui.input Nonemodel_name: ui.input Noneprompt: ui.textarea None…

番外5:下载+安装+配置Linux

任务前期工作&#xff1a; 01. 电脑已安装好VMware Workstation软件&#xff1b; 02.提前下载好Rhel-8.iso映像文件&#xff08;文件较大一般在9.4GB&#xff0c;建议采用迅雷下载&#xff09;&#xff0c;本人使用的以下版本&#xff08;地址ed2k://|file|rhel-8.4-x86_64-dvd…