【C++11】可变模板参数

目录

可变模板的定义方式

参数包的展开方式

递归的方式展开参数包

STL中的emplace相关接口函数

STL容器中emplace相关插入接口函数

​编辑

模拟实现:emplace接口


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

像之前学习的printf就是一个函数参数的可变参数,它可以接收多个任意类型,但它们只函数参数的可变参数,并不是模板的可变参数

printf的使用方法:

int printf( const char *format , ... );
本博客讲解的是函数模板的可变参数,不会涉及到类模板的可变参数

可变模板的定义方式

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

template<class ...Args> //Args全称:arguments
返回类型 函数名(Args... args)
{//函数体
}

下面就是一个基本可变参数的函数模板

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

Args:是一个可变模板参数包

args:是一个函数形参参数包

说明一下:

模板参数Args前面有省略号,代表它是一个可变模板参数,我们将带省略号的参数称为 “参数包”,这个参数包中可以包含0到任意个模板参数,args则是一个函数形参参数包

现在我们可以向这个函数中传入多个不同的类型,并且可以通过sizeof算出参数包的参数个数

以下例代码为例:

template<class ...Args>
void ShowList(Args... args)
{cout << sizeof...(args) << endl;
}int main()
{ShowList(1);ShowList(1, 2);ShowList(1, 2, string("dict"));map<string, int> m1;ShowList(1, 2, 3, m1);return 0;
}

我们无法直接获取参数包args中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

由于C++11语法不支持使用args[i]这样方式获取可变q参数,所以我们的用一些奇招来一一获取参数包的值。

错误示例:

template<class ...Args>
void ShowList(Args... args)
{//errorfor (int i = 0; i < sizeof...(args); ++i){cout << args[i] << endl;}
}

参数包的展开方式

递归的方式展开参数包

方式如下:

1.给函数模板新增一个参数,这样就可以从接收到的参数包分离出来一个参数

2.在函数模板中进行递归,不断的分离参数包中的参数

3.直到接收到最后一个参数结束

结束条件;

->1. 可以创建一个无参的函数来终止递归:当参数包中的参数为0时会调用该函数终止循环

void _ShowList()
{cout << endl;
}template<class T, class ...Args>
void _ShowList(T value, Args... args)
{cout << value << ' ';_ShowList(args...);
}int main()
{_ShowList(1, 2, string("dict"));return 0;
}

->2. 可以创建一个参数的函数来终止递归:当参数包中的参数为1时会调用该函数终止循环

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(1, 2, string("dict"));return 0;
}

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

使用sizeof...(args)算出参数个数的特性,利用它的特性做一个递归结束条件可以吗?不行!

template<class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value << ' ';if (sizeof...(args)){return;}ShowList(args...);
}

函数模板并不能调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数,这个生成的函数才能够被调用。
而这个推演过程是在编译时进行的,当推演到参数包args中参数个数为0时,还需要将当前函数推演完毕,这时就会继续推演传入0个参数时的ShowList函数,此时就会产生报错,因为ShowList函数要求至少传入一个参数。
这里编写的if判断是在代码编译结束后,运行代码时才会所走的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑。

还有一种特殊的方式,该方法比较抽象,就是使用逗号表达式展开参数包

->3. 逗号表达式展开参数包

template<class T>
void CPPprint(const T& value)
{cout << value << ' ';
}template<class ...Args>
void ShowList(Args... args)
{int array[] = {( CPPprint(args), 0)...};cout << endl;
}

当我们在数组中不标注元素个数时,编译器会帮我们自动推导元素个数,这时它会帮我们展开参数包

如下:

int array[] = {( CPPprint(args), 0), CPPprint(args), 0),  CPPprint(args), 0),  CPPprint(args), 0)};

在调用CPPprint函数的同时,利用逗号运算符的特性进行对数组的初始化

其实也可以不使用逗号运算符完成该操作

template<class T>
int CPPprint(const T& value)
{cout << value << ' ';return 0;
}template<class ...Args>
void ShowList(Args... args)
{int array[] = { (CPPprint(args))... };cout << endl;
}

将被调用的函数设置一个返回值,调用之后返回0,这样就可以在编译器展开参数包调用函数时,通过返回值初始化

STL中的emplace相关接口函数

以便大家更好的理解emplace,先给大家看一段代码,可变模板参数的使用场景:

class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Data()~构造函数" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date()~拷贝构造" << endl;}private:int _year;int _month;int _day;
};template<class ...Args>
Date* Init(Args&&... args)
{Date* ret = new Date(args...);return ret;
}int main()
{Date* p1 = Init();Date* p2 = Init(2024);Date* p3 = Init(2024, 11);Date* p4 = Init(2024, 11, 12);Date d1(2, 3, 3);Date* p5 = Init(d1);return 0;
}

我们通过将参数传入参数包在编译期间通过将参数包展开的操作进行对象的构造

STL容器中emplace相关插入接口函数

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

emplace接口全部都是使用的可变参数模板

注意:两个&&是万能引用并不是右值引用

对比list中的push_back和emplace_back,对于emplace系列接口而言,它的主要优势就是直接在容器内部构造元素可以结合我上面给的场景进行理解,而不是构造一个临时对象在复制或移动到容器中可以有效的避免拷贝和移动操作

以emplace和push_back为例:

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

调用emplace时可以传左值对象或者右值对象,但是不能使用列表初始化,emplace系列最大的特点就是,插入元素时可以传入用于构造元素的参数包

比如:

int main()
{list<pair<nxbw::string, int>> mylist;pair<nxbw::string, int> kv("nxbw", 10);mylist.emplace_back(kv); //传左值mylist.emplace_back(make_pair("nxbw", 10)); //传右值mylist.emplace_back("nxbw", 10); //传参数包mylist.push_back(kv); //传左值mylist.push_back(make_pair("nxbw", 10)); //传右值mylist.push_back({ "nxbw", 10 }); //使用列表初始化return 0;
}

原地构造:使用emplace,你可以提供构造元素所需的参数,容器会直接在emplace接口的实现中构造该对象

emplace系列接口的工作流程

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

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

emplace系列接口的意义

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

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

总结一下:

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

当然,这里的前提是容器中存储的元素所对应的类,是一个需要深拷贝的类,并且该类实现了移动构造函数。否则在调用emplace系列接口时,传入左值对象和传入右值对象的效果都是一样的,都需要调用一次构造函数和一次拷贝构造函数。

实际emplace系列接口的一部分功能和原有各个容器插入接口是重叠的,因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口,如果调用这些接口时如果传入的是右值对象,那么最终也是会调用对应的移动构造函数进行资源的移动的。

emplace接口的意义:

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

通过下面的场景我们来验证一下:

namespace nxbw
{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)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<nxbw::string, int>> mylist;pair<nxbw::string, int> kv("nxbw", 10); //构造mylist.emplace_back(kv); //传左值,mylist.emplace_back(pair<nxbw::string, int>("nxbw", 10)); //传右值mylist.emplace_back("nxbw", 10); //传参数包return 0;
}

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

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

说明一下:

模拟实现string的拷贝构造函数时复用了构造函数,因此在调用string拷贝构造的后面会紧跟着调用一次构造函数。
为了更好的体现出参数包的概念,因此这里list容器中存储的元素类型是pair,我们是通过观察string对象的处理过程来判断pair的处理过程的。
这里也可以以不同的传参方式调用push_back函数,顺便验证一下容器原有的插入函数的执行逻辑。比如:

int main()
{list<pair<nxbw::string, int>> mylist;pair<nxbw::string, int> kv("nxbw", 10);mylist.push_back(kv); //传左值mylist.push_back(pair<nxbw::string, int>("nxbw", 10)); //传右值mylist.push_back({ "nxbw", 10 }); //使用列表初始化return 0;
}

模拟实现:emplace接口

namespace nxbw
{// 模拟实现list在之前的章节有提过,这里只是将原来的代码多增加一些接口的片段代码// 这是list需要用到的节点类template<class T>struct __list_node{__list_node(const T& val = T()):_data(val), _prev(nullptr), _next(nullptr){}// 这里需要在原来的基础上需要增加一个可变模板参数模板的构造函数,方便下面使用newtemplate<class ...Args>__list_node(Args&& ...args): _data(std::forward<Args>(args)...), _prev(nullptr), _next(nullptr){}T _data;__list_node* _prev;__list_node* _next;};template<class T>struct list{template<class ...Args>iterator emplace(iterator position, Args&&... args){node* cur = position._node;node* prev = cur->_prev;// 函数参数包的完美转发node* newnode = new node(forward<Args>(args)...);prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(cur);}template<class ...Args>void emplace_back(Args&&... args){// 函数参数包的完美转发emplace(end(), forward<Args>(args)...);}// 获取节点函数,这里更新成了万能引用版的template<class T>node* get_node(T&& val = T()){node* new_node = new node(forward<T>(val)); // 完美转发new_node->_prev = new_node;new_node->_next = new_node;return new_node;}private:__list_node<T>* _head; // 指向节点类的指针};
};

emplace系列和push_back以及insert的区别

效率方面:对于左值引用版本的push_back和insert来说确实有很大的效率提升,对于右值引用版本的push_back和insert来说效率其实差不多,因为移动赋值/拷贝代价足够小

构造复杂对象:当元素的构造比叫复杂时,emplace可以让代码更简洁,直接传入构造参数即可

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

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

相关文章

python 曲线拟合,曲线拟合交点

目录 效果图: 源代码: 效果图: 源代码: import json import os import shutilimport cv2 import numpy as npfrom numpy.polynomial.polynomial import Polynomialdef calculate_distance(x1, y1, x2, y2):return np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)def get_new_g…

Java模拟Mqtt客户端连接Mqtt Broker

Java模拟Mqtt客户端基本流程 引入Paho MQTT客户端库 <dependency><groupId>org.eclipse.paho</groupId><artifactId>org.eclipse.paho.mqttv5.client</artifactId><version>1.2.5</version> </dependency>设置mqtt配置数据 …

圣诞快乐(h5 css js(圣诞树))

一&#xff0c;整体设计思路 圣诞树h5&#xff08;简易&#xff09; 1.页面布局与样式&#xff1a; 页面使用了全屏的黑色背景&#xff0c;中央显示圣诞树&#xff0c;树形由三层绿色的三角形组成&#xff0c;每一层的大小逐渐变小。树干是一个棕色的矩形&#xff0c;位于三角…

多音轨视频使用FFmpeg删除不要音轨方法

近期给孩子找宫崎骏动画&#xff0c;但是有很多是多音轨视频但是默认的都是日语&#xff0c;电视上看没办法所以只能下载后删除音轨文件只保留中文。 方法分两步&#xff0c;先安装FFmpeg在转文件即可。 第一步FFmpeg安装 FFmpeg是一个开源项目&#xff0c;包含了处理视频的…

时空信息平台架构搭建:基于netty封装TCP通讯模块(IdleStateHandler网络连接监测,处理假死)

文章目录 引言I 异步TCP连接操作II 心跳机制:空闲检测(读空闲和写空闲)基于Netty的IdleStateHandler类实现心跳机制(网络连接监测)常规的处理假死健壮性的处理假死方案引言 基于netty实现TCP客户端:封装断线重连、连接保持 https://blog.csdn.net/z929118967/article/de…

中国新能源汽车公共充电桩数据合集(2002-2023年)

数据来源&#xff1a;全国各省市统计年鉴、统计公报、国家能源署、中国汽车行业协会&#xff0c;各类汽车统计年鉴、中国电动汽车充电基础设施促进联盟等 时间跨度&#xff1a;新能源汽车数据集&#xff1a;2002-2023年&#xff08;不同数据时间跨度有差异&#xff0c;详见数据…

设计模式12:状态模式

系列总链接&#xff1a;《大话设计模式》学习记录_net 大话设计-CSDN博客 参考&#xff1a;设计模式之状态模式 (C 实现)_设计模式的状态模式实现-CSDN博客 1.概述 状态模式允许一个对象在其内部状态改变时改变其行为。对象看起来像是改变了其类。使用状态模式可以将状态的相…

国内网络在Ubuntu 22.04中在线安装Ollama并配置Open-WebuiDify

配置docker科技网络 登录后复制 创建或编辑 Docker 配置文件 让docker使用代理&#xff1a; sudo mkdir /etc/systemd/system/docker.service.d -p sudo vim /etc/systemd/system/docker.service.d/http-proxy.conf 文件&#xff0c;并添加以下内容&#xff1a; [Service] En…

【线性代数】理解矩阵乘法的意义(点乘)

刚接触线性代数时&#xff0c;很不理解矩阵乘法的计算规则&#xff0c;为什么规则定义的看起来那么有规律却又莫名其妙&#xff0c;现在参考了一些资料&#xff0c;回过头重新总结下个人对矩阵乘法的理解&#xff08;严格来说是点乘&#xff09;。 理解矩阵和矩阵的乘法&#x…

国标GB28181协议平台Liveweb:搭建建筑工地无线视频联网监控系统方案

随着科技高速发展&#xff0c;视频信号经过数字压缩&#xff0c;通过互联网宽带或者移动4G网络传递&#xff0c;可实现远程视频监控功能。将这一功能运用于施工现场安全管理&#xff0c;势必会大大提高管理效率&#xff0c;提升监管层次。而这些&#xff0c;通过Liveweb监控系统…

SQL语句练习

阅读《SQL必知必会》&#xff08;第五版&#xff09;然后结合往常表做的练习记录 这里使用的数据库时sqlite3,使用的工具时navicat 表资源链接https://wenku.baidu.com/view/349fb3639b6648d7c1c74652.html 表录入后如上图所示。后面如果有多张表之间的操作&#xff0c;在引入…

SAP RESTful架构和OData协议

一、RESTful架构 RESTful 架构&#xff08;Representational State Transfer&#xff09;是一种软件架构风格&#xff0c;专门用于构建基于网络的分布式系统&#xff0c;尤其是在 Web 服务中。它通过利用 HTTP 协议和一组简单的操作&#xff08;如 GET、POST、PUT、DELETE&…

基于MATLAB的图像增强

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;传知代码 欢迎大家点赞收藏评论&#x1f60a; 目录 一、背景及意义介绍背景图像采集过程中的局限性 意义 二、概述三、代码结构及说明&#xff08;一&#xff09;整体结构&#xff08;二&#xf…

通过阿里云 Milvus 与 PAI 搭建高效的检索增强对话系统

背景介绍 阿里云向量检索服务Milvus版&#xff08;简称阿里云Milvus&#xff09;是一款云上全托管服务&#xff0c;确保了了与开源Milvus的100%兼容性&#xff0c;并支持无缝迁移。在开源版本的基础上增强了可扩展性&#xff0c;能提供大规模 AI 向量数据的相似性检索服务。相…

滚珠花键的保养与维护方法

滚珠花键作为关键的线性运动引导装置&#xff0c;以其高精度和高刚性在众多领域发挥着举足轻重的作用。然而&#xff0c;为了保持其卓越的性能&#xff0c;保养与维护措施不可或缺。 滚珠花键的保养与维护其实就是润滑与清洁&#xff0c;以下是一些具体的保养与维护方法&#x…

Layui table不使用url属性结合laypage组件实现动态分页

从后台一次性获取所有数据赋值给 Layui table 组件的 data 属性&#xff0c;若数据量大时&#xff0c;很可能会超出浏览器字符串最大长度&#xff0c;导致渲染数据失败。Layui table 结合 laypage 组件实现动态分页可解决此问题。 HTML增加分页组件标签 在table后增加一个用于…

fastdds:idl

1使用网络收发数据的最简单方式 在学习idl之前&#xff0c;先来看一下我们在开发中&#xff0c;通过网络收发数据时&#xff0c;常常怎么实现。 struct Student {char name[32];int age;char sex;// f 男&#xff0c;m 女 };//发送侧 struct Student s1 {"xiaoming&q…

计算机网络之多路转接epoll

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 计算机网络之多路转接epoll 收录于专栏【计算机网络】 本专栏旨在分享学习计算机网络的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目…

多个Echart遍历生成 / 词图云

echart官网 安装 如果版本报错推荐安装以下版本 npm install echarts4.8.0 --savenpm uninstall echarts//这个是卸载命令以下安装成功后是局部引入:多个Echart遍历生成 vue3echart单个页面多个图表循环渲染展示:<template><div class"main"><div …

Windows server 服务器网络安全管理之防火墙出站规则设置

Windows server 服务器网络安全管理之防火墙出站规则设置 创建一条出站规则 这条出站规则针对IE浏览器设置&#xff0c;指定路径 TCP协议和指定端口&#xff08;多个端口的写法要注意&#xff09; 所有IP&#xff0c;所有应用&#xff0c;都采用阻止 给这条规则进行命名…