C++ - 可变模版参数 - emplace相关接口函数 - 移动构造函数 和 移动赋值运算符重载 的 默认成员函数

可变模版参数

我们先来了解一下,可变参数。可变参数就是在定义函数的时候,某一个参数位置使用 "..." 的方式来写的,在库当中有一个经典的函数系列就是用的 可变参数:printf()系列就是用的可变参数:

 如上图所示,printf()函数的第二个参数就是 可变参数(注意:可变参数 "..." 语法上在之前必须要有一个参数)。那么 第二个参数位置,就可以写很多个参数,在printf()函数内部就可以把第二参数位置的这些多个参数 解析出来。底层其实是用一个数组,把可变参数位置传入的实参接收的,printf()内部就会去访问这个数组,把这些参数取出来。

在 C++ 当中就需要有 可变的模版参数了。

我们说模版参数和 函数参数其实是很类似的:

  • 模版参数 是 传递类似;
  • 函数参数 是 传递对象;

 那么 可变的模版参数其实就是传入多个类型,想要几个类型, 就传入几个类型。

可变模版参数的语法

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

可变函数参数之后,要加上 Args ,这个 Args 其实是一个 模版参数包,而 在函数当中 args 是一个函数形参参数包,两者是不一样的,要注意区分。

 当然,Args 这个名字可以自定义,想取什么名字都是可以的,但是我们一般命名为 Args。其实 Args 就是 这个可变参数的 模版参数名称,跟之前取 T, K 都是一样的。

         

 如上图所示,在函数当中的 args 函数形参参数包,是用 Args 模版参数包构建出来的,如果 Args 当中只有一个类型,那么 args 当中就只会构建出一个形参,类比,如果 Args 当中只有两个类型,那么 args 当中就只会构建出两个形参。

 可变参数包在函数当中的使用

 那么,我们知道了 如果定义一个 可变模版参数,和 在函数参数列表当中传入 这个 可变模版参数,那么这个可变模版参数在函数当中如果使用呢?

template <class T, class ...Args>
void ShowList(T value, Args... args)
{}int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}

如上面这个例子,在 ShowList()这个函数模拟,到底会实例化出多少个 函数出来,我们可以在函数内部使用 sizeof()对 args 函数形参参数包 进行 大小的打印,就可以知道,当前实例化出来的 函数,有多少个参数了:
 

void ShowList(T value, Args... args)
{cout << sizeof(args) << endl;
}

但是,直接像上述一样计算 args 的大小是不行的,编译器不给这样玩,我们需要在 sizeof和 (之前加上 "..." 才行;

上述编译报错:
 

“args”: 必须在此上下文中扩展参数包

得像下面一样写: 

void ShowList(T value, Args... args)
{cout << sizeof...(args) << endl;
}

输出:

0
1
2

那么,有人就像了,既然args 实现跟数组类似,那么我们可不可以直接取出 args 当中的数据呢?

答案是不行的,它不给直接取出args 的数据,必须要扩展上下文(具体在后面阐述):

 编译报错:
 

“args”: 必须在此上下文中扩展参数包

利用模版参数的推演,取出 参数包当中的值

 对于 扩展参数包,除了可以像上述一样 在前置加 "..." 的方式,扩展 参数包;

我们还可以利用编译器对模版参数的推演,来取出参数包当中的类型。

具体做法就是在 可变模版参数之前,多加一个 模版参数,利用编译器对这个模版参数的推演,扩展出 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(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}

如果之传入的一个参数,那么直接调用 第一个 ShowList(const T& val);如果传入的是多个参数,那么就会调用 void ShowList(T value, Args... args) 这个函数,在这个函数当中,就会把第一个参数 推导出来,打印,然后又去调用 void ShowList(T value, Args... args) 这个函数,但是在传入参数的时候值传入 参数包,相当于是把 第一个参数舍弃了,然后传参,这样再下一层函数,就会从参数包当中取出第一个数据。

直到,参数包当中数据区得只剩下 一个,那么就会调用 第一个 结合条件的函数。相当于是 利用 第一个模版参数 一直推导出 args 当中的类型。

 但是,可变模版参数很少用到,上诉过程可以制作理解。

但是上述取出 args 的方法只能取出有参数的 例子,如果是无参传入的话,就会不匹配了,所以我们对结束函数进行改进,把结束函数改为  无参的 函数:
 

void ShowList()
{cout << endl;
}

还有一种新的方式:

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;
}

这种展开参数包的方式,不需要通过递归终止函数,是直接在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数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
 

如果不想用 逗号表达式的话,可以利用 printarg()函数的返回值,返回一个 0:

template <class T>
int PrintArg(T t)
{cout << t << " ";return 0;
}//展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { PrintArg(args)... };cout << endl;
}

empalce相关接口函数

我们先来看一个 可变模版参数的引用场景:

class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Date构造" << 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* Create(Args... args)
{Date* ret = new Date(args...);return ret;
}int main()
{Date* p1 = Create();Date* p2 = Create(2023);Date* p3 = Create(2023, 9);Date* p4 = Create(2023, 9, 27);Date d(2023, 1, 1);Date* p5 = Create(d);return 0;
}

当 Data 当中的构造函数是这样写的时候:

Date(int year , int month , int day )    ->   外部函数当中传入三个参数:Date* p4 = Create(2023, 9, 27);

 也就是,传入三个参数,那么 在 Create 当中的 参数包就会接受这三个参数,然后传入到 Data 构造函数当中;

如果传入的不是 三个参数(    Date* p1 = Create();   Date* p2 = Create(2023);  Date* p3 = Create(2023, 9);),就会报错。

但是,在上述例子当中吗,我们是吧 data 的构造函数当中的三个 参数都些都写成是 缺省的,所以在上述例子的当中才可以那么自由的使用 Create ()函数。


STL容器当中,还有 empalce相关接口函数:
cplusplus.com/reference/vector/vector/emplace_back/

cplusplus.com/reference/list/list/emplace_back/

template <class... Args>
void emplace_back (Args&&... args);

首先我们看到的emplace系列的接口,支持模版的可变参数,并且,函数当中的形参是万能引用,那么 emplace_back()接口相对于 insert()有那些提升呢?

int main()
{std::list< std::pair<int, char> > mylist;// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别mylist.emplace_back(10, 'a');mylist.emplace_back(20, 'b');mylist.emplace_back(make_pair(30, 'c'));mylist.push_back(make_pair(40, 'd'));mylist.push_back({ 50, 'e' });for (auto e : mylist)cout << e.first << ":" << e.second << endl;return 0;
}

push_back ()函数,需要传入一个 make_pair ()构造一个 pair 对象传入,才能进行尾插;但是 在 emplace_back()接口,就直接传入参数,它会被 参数包接受,然后在 解析出来。但是,上述除了 在用法上不同,实际上和 push_back()是差不多的。

int main()
{// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back// 是先构造,再移动构造,其实也还好。std::list< std::pair<int, bit::string> > mylist;mylist.emplace_back(10, "sort");mylist.emplace_back(make_pair(20, "sort"));mylist.push_back(make_pair(30, "sort"));mylist.push_back({ 40, "sort" });return 0;
}

也就是说, emplace_back()能做到,直接引用参数来对其中的结点进行构造;但是 push_back()必须先进行构造,然后在进行拷贝构造。

 按照上述来看,emplace_back()还是有优化的,但是别忘了 ,C++11 之后是可以使用 移动拷贝的,想上述 push_back()先构造,在进行拷贝构造,就会被直接优化为 空间的 两个指针的交换,这个代价就太低了,相对来说,emplace_back()就不够看了。

 

 移动构造函数 和 移动赋值运算符重载 的 默认成员函数

在C++ 11之前,有 6 大默认成员函数:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值重载
  • 取地址重载
  • const 取地址重载

C++11 增加有值引用之后,就有了移动语义的概念,所谓移动语义就是  移动构造函数 和 移动赋值运算符重载函数,两函数。

那么所以默认成员函数的话,我们写了,那好说,就使用我们定义的 函数;如果我们没有写,那么编译器就会自动生成一个,这个自动生成是什么条件下的才会自动生成的?

  • 如果你没有自己实现移动构造函数没有实现析构函数拷贝构造拷贝赋值重载中的任意一个那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
     

这里和 之前默认成员函数一样,对于浅拷贝(也就是只拷贝内置类型),那么我们可以放心大胆的交给编译器去自动生成,就够用了;如果是深拷贝(比如自己开得有 堆空间),那么就需要我们自己定义   移动构造函数 和 移动赋值运算符重载函数。

如果我们都实现析构函数拷贝构造拷贝赋值重载  但是我们又想编译器自己生成一个 移动构造函数 或者是 移动赋值重载函数 的话,我们可以这样写:

string(Person&& p) = default;

上述只给出了   移动构造函数 的方式,移动赋值重载函数 的方式也是类似的。

这样可以让编译器前置生成一个 移动构造函数 或者是 移动赋值重载函数

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

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

相关文章

docker安装运行环境相关的容器

docker安装常用软件步骤 docker安装Tomcat:latest 2023-10-09 1&#xff09;搜索镜像 以Tomcat为例子&#xff0c;先去官网仓库搜索https://hub.docker.com/search?qtomcat 或者直接命令查询 docker search tomcat2&#xff09;拉取镜像 docker pull tomcat3&#xff09…

【git】git命令行

首先要了解git整个流程的一个分类&#xff1a; workspace&#xff1a;工作区staging area&#xff1a;暂存区/缓存区local repository&#xff1a;版本库或本地仓库remote repository&#xff1a;远程仓库 创建仓库 git clone gitgithub.comxxxxxxxxxxxx//拷贝一份远程仓库 …

qt 5.15.2 安卓 macos

macos环境安卓配置 我的系统是monterey12.5.1 打开qt的配置界面 这里版本是java1.8&#xff0c;注意修改这个json文件&#xff0c;显示包内容 {"common": {"sdk_tools_url": {"linux": "https://dl.google.com/android/repository/comm…

使用vite+npm封装组件库并发布到npm仓库

组件库背景&#xff1a;使用elementplusvue封装了一个通过表单组件。通过JSX对el-form下的el-input和el-button等表单进行统一封装&#xff0c;最后达到&#xff0c;通过数据即可一键生成页面表单的功能。 1.使用vite创建vue项目 npm create vitelatest elementplus-auto-form…

React核心原理与实际开发

学习目标 React是啥&#xff1f; 官方定义&#xff1a;将前端请求获取到的数据渲染为HTML视图的JavaScript库。 一、React入门 1、React项目创建 直接创建react&#xff0c;使用初始化会创建package.json npm init -y再安装 2、React基本使用 使用纯JS创建ReactDOM&#…

[PowerQuery] PowerAutoMate 刷新PowerBI 数据

通过PowerBI Automate 进行PowerBI 数据刷新之前,需要有Power Automate 授权或者Power Automate 试用账户,可以通过如下的地址进行申请注册。 https://flow.microsoft.com/zh-cn/ 完成Power Automate 登录之后,选中计划的云端流后创建,图为创建计划的云端流的操作步骤。 …

JVM CMS和G1执行过程比较

CMS CMS&#xff08;Concurrent Mark Sweep&#xff09;收集器是一种以获取最短回收停顿时间为目标的收集器。由于大部分 Java 应用主要集中在互联网网站以及基于浏览器的 B/S 系统的服务端&#xff0c;这类应用通常会较为关注服务的响应速度&#xff0c;希望系统的停顿时间尽…

JS进阶-原型

原型 原型就是一个对象&#xff0c;也称为原型对象 构造函数通过原型分配的函数是所有对象所共享的 JavaScript规定&#xff0c;每一个构造函数都有一个prototype属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象 这个对象可以挂载函数&#xff0c;对象实…

10-Node.js入门

01.什么是 Node.js 目标 什么是 Node.js&#xff0c;有什么用&#xff0c;为何能独立执行 JS 代码&#xff0c;演示安装和执行 JS 文件内代码 讲解 Node.js 是一个独立的 JavaScript 运行环境&#xff0c;能独立执行 JS 代码&#xff0c;因为这个特点&#xff0c;它可以用来…

基于SpringBoot的精准扶贫管理系统

目录 前言 一、技术栈 二、系统功能介绍 用户信息管理 贫困户信息管理 新闻类型管理 志愿者招聘管理 志愿者招聘 留言反馈管理 贫困户 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息…

c#设计模式-行为型模式 之 状态模式

&#x1f680;简介 状态模式是一种行为设计模式&#xff0c;它允许对象在其内部状态改变时改变其行为&#xff0c;我们可以通过创建一个状态接口和一些实现了该接口的状态类来实现状态模式。然后&#xff0c;我们可以创建一个上下文类&#xff0c;它会根据其当前的状态对象来改…

CSS3实现动画加载效果

<!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>加载效果</title><link rel"style…