C++——移动构造和完美转发

news2024/11/14 19:00:37

1.什么是右值

右值引用是C++11的概念,与之对应的是左值引用。

当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存当中的位置)。

以上的概念是摘录自《C++ primer》。

但是这样的概念并不足以理解。用一句简单的话描述左值和右值:左值都是可以被取地址的;右值都是不可以被取地址的。

根据结论可以快速判断出哪些是左值、哪些是右值:

int func() { return 100; }
int main()
{
	int x = 3;// 可以取地址,左值
	string s("hello");// 可以取地址、左值
	string("world");// 不可取地址、右值
	12;// 不可取地址、右值
	func();// 返回值不可取地址,右值
	return 0;
}

2.移动构造和移动赋值

左值引用只能引用左值,右值引用只能右值、const左值引用可以引用左值也可以引用右值。

单纯的右值引用没有意义。

右值引用的使用场景在于移动构造和移动赋值。

移动构造对应拷贝构造、移动赋值对应赋值运算符重载。

上面的概念是有问题的、不准确的。把他们放在一起的原因是要讨论"移动"和"拷贝"的区别。"移动"区别于"拷贝","移动"是不会发生拷贝的,它更像是一种"窃取"、"转移"。

也就是说,把右值的内容"转移"到其他地方去,从而减少不必要的拷贝。写一份伪代码和画一幅图来理解移"移动":

class String
{
    char *_str;
};

int main()
{
    String s1(String("hello"));
}


 

 如上图,s1对象在调用构造的时候不会调用拷贝构造(如果实现了移动构造),s1当中的_str成员不会指向new出来的空间,而是指向匿名对象当中_str所指向的空间。

那么从上图看来会有一个潜在的问题,那就是有两个指针指向同一块空间,会有重复析构的风险。所以在编写移动构造的时候,要使资源被移动的对象能够正确析构(在这个例子中让_str指向空就行了)。

这里给出一段String类的移动构造吧:

String(String&& s)
{
	swap(s);
	s._str = nullptr;
	s._size = s._capacity = 0;
	cout << "String:移动构造" << endl;
}

void swap(String& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);    
}

对于移动赋值来说也是一样的道理,都是对右值进行资源转移。这里给出String类的赋值运算符重载代码实现:

String &operator=(String s)
{
	swap(s);
	cout << "String:移动赋值" << endl;
	return *this;
}

这个写法是C++的现代写法,形参是一个对象并不是引用,所以在传参的时候会调用拷贝构造或者是移动构造(如果实参是右值的话),然后在移动赋值内部进行资源转移。

3.左值"转化"为右值

C++11提供了一个可以将左值"转化"为右值的接口,即std::move()。

实际上"转化"的说法是不对的,因为std::move()的返回值是一个实参的一个右值引用。

int main()
{
    String s1("hello");
    String s2("world");
    String s2 = std::move(s1);
}

在上面的代码当中,s2的赋值操作不会调用赋值重载,而是调用移动赋值。注意std::move()并不是将左值真实的转化为右值,而是返回左值的右值引用。

使用std::move()需要注意一件事情,就拿上面的代码来说,s1赋值给s2,移动赋值之后s1的指向不再指向"hello",而是指向"world"或者置空。

也就是说使用std::move()并且移动赋值给其他对象的对象,移动赋值结束之后最好不要使用它。

4.左值or右值?

有一个匪夷所思但是真实存在的一个问题,看下面的代码:

String(String&& s)
{
	swap(s);
	s._str = nullptr;
	s._size = s._capacity = 0;
	cout << "String:移动构造" << endl;
}

能够调用移动构造一定是实参是一个右值,那么拿匿名对象为例,它本身没有名字,也就无法直接使用它。但是它移动构造是一个右值引用,既然是引用那么它就是一个对象的别名。

所以得出一个结论,即右值传递给右值引用为参数的函数后,在该函数内部作为左值使用。

但是如果在这个函数内部就是想把它当做右值并且传递给其他函数来使用的话该怎么办?

5.完美转发

在C++11之前就有了万能引用的概念,C++11之后,万能引用的概念更加贴切了。

万能引用既可以引用左值、也可以引用右值。

它的写法就是一个模板类型的右值引用:

template <class T>
void func(T &&t)// 即可以引用左值、也可引用右值
{
	cout << t << endl;
}

int main()
{
	func(15);// 传递右值
	string s1("nice");
	func(s1);// 传递左值
	return 0;
}

但是刚才说过,右值传递并且进入到func函数之后,它就变成了左值,那么如果在func函数内部又调用了一个函数,但是该函数的参数部分只接收右值引用,该怎么办?

void Print(int&& t)
{
	cout << t << endl;
}
template <class T>
void func(T &&t)// 即可以引用左值、也可引用右值
{
	Print(t);
}

如果像上面这么写就会喜提报错: 

 所以C++11提供了一个方法,即std::forword<T>()方法。它的作用就是实现完美转发,功能就是保持函数模板参数的原有属性(片面理解,这里涉及到引用折叠,稍后解释)。

将上面的代码进行整改:

void Print(int&& t)
{
	cout << t << endl;
}
template <class T>
void func(T &&t)// 即可以引用左值、也可引用右值
{
	Print(std::forward<T>(t));
}

int main()
{
	func(15);// 传递右值
	return 0;
}

那么完美转发的应用场景是什么呢?这里给出一个场景:构造转发,即将构造函数设计成函数模板,使用完美转发将参数保持原样属性传递给其他对象的构造函数。

给出一段代码实现:

template <class Tname,class Tage,class Tsex>
User(Tname &&name, Tage &&age, Tsex &&sex)
	:_name(std::forward<Tname>(name)),
	_age(std::forward<Tage>(age)),
	_sex(std::forward<Tsex>(sex))
{}

如上展示了一个User类的构造函数,暂且不需要关心_name、_age、_sex的具体类型。

反正在这个构造函数当中,name、age、sex所指向的对象都保持原有属性传递给了_name、_age和_sex的构造函数(如果指向的对象是右值的话,那么就是保持右值传递给移动构造)。

所以说,完美转发也可以解决不必要的拷贝问题。

6.引用折叠

引用总共就两种类型嘛,左值引用和右值引用。

注意看上面写的完美转发的函数模板,参数是"模板参数类型的右值引用"。那么模板参数类型无非就三种,非引用类型、左值引用、右值引用,所以引用折叠就是模板参数类型和后面所跟的&&结合起来:

template<calss T>
&&    // T不是引用类型
& &&    // T是左值引用
&& &&    // T是右值引用

在上面的伪代码中,可以得知:只有模板为左值引用的时候才会折叠成左值引用,其他的都是右值引用。

即,折叠后为"&&",右值引用;折叠后为"& &&",左值引用;折叠后为"&& &&",右值引用。

但是引用折叠是不能在代码当中体现的,它只能在一些间接转换的场景当中存在。例如:

int main()
{
	int x = 3;
	int& && rx = x;// 虽然这引用折叠最后会成为左值引用,但是不能在代码体现出来
	return 0;
}

上面的代码是会报错的。

所以可以得出几个结论:

  1. 如果传递的是右值,那么模板参数类型就为&&,最后折叠,函数模板的参数就为&&
  2. 如果传递的是左值,那么模板参数类型必须为&,最后折叠,函数模板的参数就为& &&,是一个左值引用。传递左值并且模板参数类型如果不是引用类型的话,那么折叠之后就是&&了,这是不对的

7.forward的原理

看一个例子:

template <class T>
void func(T &&t)// 即可以引用左值、也可引用右值
{
	Print(std::forward<T>(t));
}

再看一下forward的原型:

这里就很容易猜出forward的大概原理了。

拿上面的代码来说,如果传递给func的实参是右值,那么T的类型的就是&&,和后面的&&折叠, 最终是一个右值引用,那么func的完整写法就该是这样的:

template <class T>
void func(int&& &&t)// 假设实参是int类型
{
	Print(std::forward<T>(t));
}

但是要注意这样的写法是错误的,只是做一个演示。

虽然知道了t是一个右值引用,但是刚才说过,在函数内部只能当成左值来使用,所以在调用forward<T>的时候,匹配的是第一个重载函数。

在forward<T>方法内部,会进行一个类型转换:

template<typename T>
T&& forward(T &param)// forward的实现原理
{
	return static_cast<T&&>(param);
}

因为在调用forward的时候,即func的模板参数类型T为&&,所以forward的模板参数类型T也为&&。forward内部进行一个强制类型转换,param原本是个左值引用嘛,然后强转成&& &&类型,所以forward的返回值就是一个右值引用。这样就实现了forward保持参数原有属性的功能。

同样的,如果传递给func的实参就是一个左值,那么func、forward的模板参数类型T为&,所以在forward内部当中,所以param就强转成了& &&类型,是一个左值引用。

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

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

相关文章

【SentenceTransformer系列】计算句子嵌入的概念(01/10)

一、说明 要分清词嵌入和句子嵌入的区别。 句子嵌入是指将句子或文档表示为固定长度的向量的过程&#xff0c;使得向量能够捕获句子的语义和上下文信息。它是自然语言处理 (NLP) 和机器学习中的常见任务&#xff0c;因为它可以帮助对句子之间的关系和相似性进行建模&#xff0c…

接口自动化测试(添加课程接口调试,调试合同上传接口,合同列表查询接口,批量执行)

1、我们把信息截取一下 1.1 添加一个新的请求 1.2 对整个请求进行保存&#xff0c;Ctrl S 2、这一次我们添加的是课程添加接口&#xff0c;以后一个接口完成&#xff0c;之后Ctrl S 就能够保存 2.1 选择方法 2.2 设置请求头&#xff0c;参数数据后期我们通过配置设置就行 3、…

Lua 位和字节

一、位运算 从 Lua 5.3 版本开始&#xff0c;提供了针对数值类型的一组标准位运算符&#xff0c;与算数运算符不同的是&#xff0c;运算符只能用于整型数。 运算符描述&按位与|按位或&#xff5e;按位异或>>逻辑右移<<逻辑左移&#xff5e;&#xff08;一元运…

安全学习DAY17_信息打点-语言框架组件识别

信息打点-WEB打点-语言框架&开发组件 文章目录 信息打点-WEB打点-语言框架&开发组件本节涉及链接&工具本节知识&思维导图基础概念介绍框架&#xff1a;组件&#xff1a;Web架构 对应Web测试手法后端&#xff1a;前端组件&#xff1a;java居多&#xff0c;框架&…

RP2040开发板自制树莓派逻辑分析仪

目录 前言 1 准备工作和前提条件 1.1 Raspberry Pi Pico RP2040板子一个 1.2 Firmware-LogicAnalyzer-5.0.0.0-PICO.uf2固件 1.3 LogicAnalyzer-5.0.0.0-win-x64软件 2 操作指南 2.1 按住Raspberry Pi Pico开发板的BOOTSEL按键&#xff0c;再接上USB接口到电脑 2.2 刷入…

产品帮助中心怎么做?这两点不能忽略,让用户自助解决问题!

对于大部分线上产品&#xff0c;因为其功能和系统的复杂性&#xff0c;使得新手客户入门学习非常复杂&#xff0c;为了快速响应并且解决问题&#xff0c;一套系统完整的产品帮助中心必不可少&#xff01; 产品帮助中心 因此&#xff0c;对于很多产品开发者来说&#xff0c;借助…

pg简单使用

1.创建服务器 2.创建数据库 3.修改默认连接数据库 工具都是链接到这里 4.数据库代码工具

ByteBuffer 使用

ByteBuffer 使用 1 java.nio包中的类定义的缓冲区类型2 缓冲区常用属性2.1缓冲区的容量(capacity)2.2 缓冲区的位置(position)2.3 缓冲区的限制(limit)2.4 缓冲区的标记(mark)2.5 剩余容量 remaining/hasRemaining 3 缓冲区常用方法3.1 创建缓冲区3.1.1 allocate方法3.1.2 wrap…

交叉编译之wiringPi库,【全志H616,orangepi-zero2】

文章目录 书接上回wiringPi全志库下载建立软链接软连接软连接创建 硬链接硬链接创建 测试树莓派运行servo文件 结束 书接上回 上回已经完整的安装了全志的gcc交叉编译工具 https://blog.csdn.net/qq_52749711/article/details/132306764 wiringPi全志库下载 下载链接 先搞到…

Jmeter+ant+jenkins实现持续集成

jmeterantjenkins持续集成 一、下载并配置jmeter 首先下载jmeter工具&#xff0c;并配置好环境变量&#xff1b;参考&#xff1a;https://www.cnblogs.com/YouJeffrey/p/16029894.html jmeter默认保存的是.jtl格式的文件&#xff0c;要设置一下bin/jmeter.properties,文件内容…

中国电信物联网收入33亿元,用户达到4.73亿户!

近日&#xff0c;中国电信发布2023中期业绩&#xff0c;物联网迎来强劲增长&#xff0c;物联网收入33亿元&#xff0c;同比增长75.7%&#xff0c;物联网用户4.73亿户&#xff0c;同比增长31.5%。天翼物联自主研发的AIoT物联网平台&#xff0c;升级为云原生3AZ架构&#xff0c;提…

在线课堂录播直播管理系统SpringBoot+Vue

在线课堂录播直播管理系统SpringBootVue 文章目录 在线课堂录播直播管理系统SpringBootVue共三个端&#xff1a;后端、后台管理系统、前端&#xff0c;如要学习看评论区&#xff08;全部源码、文档、数据库&#xff09;。内置功能一、前端二、后台管理三、后端--代码全有。四、…

k8s 认证和权限控制

k8s 的认证机制是啥&#xff1f; 说到 k8s 的认证机制&#xff0c;其实之前咋那么也有提到过 ServiceAccouont &#xff0c;以及相应的 token &#xff0c;证书 crt&#xff0c;和基于 HTTP 的认证等等 k8s 会使用如上几种方式来获取客户端身份信息&#xff0c;不限于上面几种…

【数据结构OJ题】链表分割

原题链接&#xff1a;https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70?tpId8&&tqId11004&rp2&ru/activity/oj&qru/ta/cracking-the-coding-interview/question-ranking 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2…

Java面向对象——封装以及this关键字

封 装 封装是面向对象编程&#xff08;OOP&#xff09;的三大特性之一&#xff0c;它将数据和操作数据的方法组合在一个单元内部&#xff0c;并对外部隐藏其具体实现细节。在Java中&#xff0c;封装是通过类的访问控制修饰符&#xff08;如 private、protected、public&#x…

Android Drawable转BitmapDrawable再提取Bitmap,Kotlin

Android Drawable转BitmapDrawable再提取Bitmap&#xff0c;Kotlin <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"…

C++ 结构体的对齐

C 结构体的对齐 flyfish 文章目录 C 结构体的对齐一 非对齐方式二 对齐方式示例1示例2 三 对齐到指定字节数 boundary 一 非对齐方式 也就是按照1字节对齐 #pragma pack(1) typedef unsigned char BYTE; typedef struct message {BYTE a[4];BYTE b[2];BYTE *c;BYTE d[4];} M…

阿里云ECS服务器企业级和共享型介绍_企业级常见问题解答FAQ

阿里云企业级服务器是什么&#xff1f;企业级和共享型有什么区别&#xff1f;企业级服务器具有独享且稳定的计算、存储、网络资源&#xff0c;如ECS计算型c6、通用型g8等都是企业级实例&#xff0c;阿里云百科分享什么是企业级云服务器、企业级实例的优势、企业级和共享型云服务…

如何收缩wsl2虚拟磁盘

简介 WSL2使用虚拟化层为它带来更高的性能和兼容性。但是&#xff0c;WSL2 的少数缺点之一是它使用虚拟磁盘 &#xff08;VHDX&#xff09; 来存储文件系统。这意味着您的虚拟磁盘占用了 100GB&#xff0c;但 WSL2 只需要 15GB... 所以要寻找一种缩小 WSL2 虚拟磁盘的方法&…

​Redis概述

目录 Redis - 概述 使用场景 如何安装 Window 下安装 Linux 下安装 docker直接进行安装 下载Redis镜像 Redis启动检查常用命令 Redis - 概述 redis是一款高性能的开源NOSQL系列的非关系型数据库,Redis是用C语言开发的一个开源的高键值对(key value)数据库,官方提供测试…