C++11之可变参数模板

可变参数模板

  • 可变参数模板概念
  • 可变参数模板定义
  • 参数包展开方式
    • 递归展开参数包
    • 逗号表达式展开参数包
  • 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.rhkb.cn/news/143137.html

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

相关文章

铁红面建筑模板:木质之美与实用性的结合

铁红面建筑模板&#xff0c;又称为铁红面板或酚醛面建筑模板&#xff0c;是一种常用的建筑施工材料。其主要特点是选用进口辐射松、樟子松或国内的马尾松旋切木材作为面皮&#xff0c;经过胶机上的面胶和热压机的压合而成。这种建筑模板在施工中发挥着重要的作用&#xff0c;并…

基于ModebusRTU通信采集温度湿度项目案例

目录 一、模拟温湿度模拟 【1.1】温湿度仪表参数 【1.1】使用电脑模拟传感器 【1.2】使用Codesys软件模拟传感器 二、自定义控件UI设计 【2.1】自定义控件温度湿度柱状设计 ​编辑 【2.1.1】设置温度湿度柱状实际显示【属性】 【2.1.2】设置温度湿度柱状的背景颜色【属…

win10系统 C++环境 安装编译GRPC

第一步 下载源码、更新、cmake编译&#xff1a; 为了依赖的成功安装&#xff0c;采用gitee进行下载与更新。记得需要安装git软件。 安装命令&#xff1a; 在自己指定的目录下&#xff0c;鼠标右键&#xff0c;选择 git Bash Here 打开命令行 git clone -b v1.34.0 https://gi…

MacOS上的Pip和Python升级指南

在MacOS系统上&#xff0c;保持Pip和Python版本的最新状态对于顺利进行Python开发至关重要。通过升级Pip和Python&#xff0c;你可以享受到最新的功能、修复的bug以及提升的开发效率。本文将为你提供在MacOS上升级Pip和Python的详细指南&#xff0c;助你打造更强大的开发环境。…

React redux更新数据的诡异特征==》彻底掌握redux更新state机制的精髓

此文章是跟随我上一篇文章《Redux Toolkit中action派发但state值不更新的原因》写的。 本来一切都搞定了&#xff0c;此时我突发奇想&#xff1a; 如果让api服务端不发送包含x-pagination的header信息&#xff0c;web端会不会报错。因为按照web端 redux原有的逻辑&#xff1a;…

Kubernetes(K8s):未来云原生应用的引擎

文章目录 Kubernetes的核心概念和架构为什么K8s是构建云原生应用的首选工具&#xff1f;云原生应用的好处和挑战容器编排的重要性&#xff1a;Docker和KubernetesKubernetes生态系统&#xff1a;核心组件和附加工具实际应用&#xff1a;企业如何在生产环境中使用K8s未来展望&am…

Flutter实现PS钢笔工具,实现高精度抠图的效果。

演示&#xff1a; 代码&#xff1a; import dart:ui;import package:flutter/material.dart hide Image; import package:flutter/services.dart; import package:flutter_screenutil/flutter_screenutil.dart; import package:kq_flutter_widgets/widgets/animate/stack.dart…

任意文件的上传和下载

1.任意文件下载&#xff08;高危&#xff09; 定义 一些网站由于业务需求&#xff0c;往往需要提供文件查看或文件下载功能&#xff0c;但若对用户查看或下载的文件不做限制&#xff0c;则恶意用户就能够查看或下载任意敏感文件&#xff0c;这就是文件查看与下载漏洞。 可以下载…

Kafka 常见问题

文章目录 kafka 如何确保消息的可靠性传输Kafka 高性能的体现利用Partition实现并行处理利用PageCache 如何提高 Kafka 性能调整内核参数来优化IO性能减少网络开销批处理数据压缩降低网络负载高效的序列化方式 kafka 如何确保消息的可靠性传输 消费端弄丢了数据 唯一可能导致…

8、SpringBoot_多环境开发

二、多环境开发 1.概述 概述&#xff1a;开发环境、测试环境、生产环境 分类 开发环境 spring:datasource:druid:url: jdbc:mysql://localhost:3306/springboot_ssmusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver测试环境 spring:datasource:dr…

【PickerView案例10-国旗选择界面02 Objective-C预言】

一、好了,我们继续来实现这个国旗选择界面: 1.它的界面里面,是不是很简单,就一个UIPickerView,就完事儿了 然后,显示的每一行内容呢, 1)一个文字Label 2)一个图片 那大家应该有意识,它返回的应该是一个View,对吧, 代理方法里面,有一个返回View的,viewForRow…

【VUE复习·2】@click 之事件处理与函数(可传参);@click 阻止事件冒泡应用场景;@click 多修饰符应用场景(高级)

总览 1.“事件处理”是什么 2.click 函数参数传递应用 3.click 阻止事件冒泡应用场景 4.click 多修饰符应用场景&#xff08;高级&#xff09; 一、“事件处理”是什么 1.概念 我们在和页面进行交互时&#xff0c;进行点击或滑动或其他动作时&#xff0c;我们操作的是 DOM …

Ajax

一、什么是Ajax <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-wid…

[python 刷题] 853 Car Fleet

[python 刷题] 853 Car Fleet 哎……周赛第三题解应该用 monotonic stack 做优化的&#xff0c;没写出来&#xff0c;所以多刷两题 monotonic stack 的题目找找感觉…… 题目&#xff1a; There are n cars going to the same destination along a one-lane road. The destin…

MybatisPlus自定义SQL用法

1、功能概述&#xff1f; MybatisPlus框架提供了BaseMapper接口供我们使用&#xff0c;大大的方便了我们的基础开发&#xff0c;但是BaseMapper中提供的方法很多情况下不够用&#xff0c;这个时候我们依旧需要自定义SQL,也就是跟mybatis的用法相同&#xff0c;自定义xml映射文…

win11+wsl+git+cmake+x86gcc+armgcc+clangformat+vscode环境安装

一、安装wsl &#xff08;1&#xff09;打开power shell 并运行&#xff1a; Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform &#xff08;2&#xff0…

APP开发费用估算方法

估算APP开发费用是一个重要的项目管理步骤&#xff0c;它有助于确定项目的总成本&#xff0c;并帮助您在项目规划阶段做出决策。APP开发费用估算的方法可以根据项目的规模、复杂性、功能和技术选择而异&#xff0c;以下是一些常见的APP开发费用估算方法&#xff0c;希望对大家有…

tailwind使用教程以及tailwind不生效的问题

以Vite项目为例 我们先安装依赖文件 生成文件 yarn add -D tailwindcss postcss autoprefixer npx tailwindcss init -p配置tailwind.config.js文件 /** type {import(tailwindcss).Config} */ export default {content: ["./index.html","./src/**/*.{vue,j…

Win/Mac版Scitools Understand教育版申请

这里写目录标题 前言教育版申请流程教育账号申请 前言 上篇文章为大家介绍了Scitools Understand软件&#xff0c;通过领取的反馈来看有很多朋友都想用这个软件&#xff0c;但是我的网盘里只存了windows的pojie版&#xff0c;没有mac版的&#xff0c;我没有去网上找相关的资源…

【00】FISCO BCOS区块链简介

官方文档&#xff1a;https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/docs/introduction.html FISCO BCOS是由国内企业主导研发、对外开源、安全可控的企业级金融联盟链底层平台&#xff0c;由金链盟开源工作组协作打造&#xff0c;并于2017年正式对外开源。 F…