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

本文主要是介绍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;

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

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

这篇关于C++ - 可变模版参数 - emplace相关接口函数 - 移动构造函数 和 移动赋值运算符重载 的 默认成员函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/178917

相关文章

Pandas中统计汇总可视化函数plot()的使用

《Pandas中统计汇总可视化函数plot()的使用》Pandas提供了许多强大的数据处理和分析功能,其中plot()函数就是其可视化功能的一个重要组成部分,本文主要介绍了Pandas中统计汇总可视化... 目录一、plot()函数简介二、plot()函数的基本用法三、plot()函数的参数详解四、使用pl

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Python中的Walrus运算符分析示例详解

《Python中的Walrus运算符分析示例详解》Python中的Walrus运算符(:=)是Python3.8引入的一个新特性,允许在表达式中同时赋值和返回值,它的核心作用是减少重复计算,提升代码简... 目录1. 在循环中避免重复计算2. 在条件判断中同时赋值变量3. 在列表推导式或字典推导式中简化逻辑

SpringBoot请求参数接收控制指南分享

《SpringBoot请求参数接收控制指南分享》:本文主要介绍SpringBoot请求参数接收控制指南,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring Boot 请求参数接收控制指南1. 概述2. 有注解时参数接收方式对比3. 无注解时接收参数默认位置

Python的time模块一些常用功能(各种与时间相关的函数)

《Python的time模块一些常用功能(各种与时间相关的函数)》Python的time模块提供了各种与时间相关的函数,包括获取当前时间、处理时间间隔、执行时间测量等,:本文主要介绍Python的... 目录1. 获取当前时间2. 时间格式化3. 延时执行4. 时间戳运算5. 计算代码执行时间6. 转换为指

Python正则表达式语法及re模块中的常用函数详解

《Python正则表达式语法及re模块中的常用函数详解》这篇文章主要给大家介绍了关于Python正则表达式语法及re模块中常用函数的相关资料,正则表达式是一种强大的字符串处理工具,可以用于匹配、切分、... 目录概念、作用和步骤语法re模块中的常用函数总结 概念、作用和步骤概念: 本身也是一个字符串,其中

Python使用getopt处理命令行参数示例解析(最佳实践)

《Python使用getopt处理命令行参数示例解析(最佳实践)》getopt模块是Python标准库中一个简单但强大的命令行参数处理工具,它特别适合那些需要快速实现基本命令行参数解析的场景,或者需要... 目录为什么需要处理命令行参数?getopt模块基础实际应用示例与其他参数处理方式的比较常见问http

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

CentOS7更改默认SSH端口与配置指南

《CentOS7更改默认SSH端口与配置指南》SSH是Linux服务器远程管理的核心工具,其默认监听端口为22,由于端口22众所周知,这也使得服务器容易受到自动化扫描和暴力破解攻击,本文将系统性地介绍... 目录引言为什么要更改 SSH 默认端口?步骤详解:如何更改 Centos 7 的 SSH 默认端口1