【C++11】右值引用和移动语义 {左值引用和右值引用;移动语义;解决函数传值返回的深拷贝问题;完美转发}

一、左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

  • 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取左值的地址,可以对左值赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边。定义const修饰的左值,不能赋值,但是可以取地址。能取地址的就是左值

  • 左值引用就是给左值的引用,给左值取别名。

  • 左值引用只能引用左值,不能引用右值。

  • 但是const左值引用既可引用左值,也可引用右值。

int main()
{// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; // ra为a的别名//int& ra2 = 10; // 编译失败,因为10是右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0;
}

什么是右值?什么是右值引用?

  • 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(传值返回) 等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址,不能赋值

  • 右值引用就是对右值的引用,给右值取别名。

  • 右值引用只能引用右值,不能引用左值。

  • 但是右值引用可以引用move以后的左值。

int main()
{double x = 1.1, y = 2.2;// 以下几个都是常见的右值10;x + y;fmin(x, y);// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);//右值不能取地址,不能赋值// 这里编译会报错:error C2106: “=”: 左操作数必须为左值&10;10 = 1;x + y = 1;fmin(x, y) = 1;// 右值引用只能右值,不能引用左值。int&& r1 = 10;//int a = 10; //int&& r2 = a;	// error:无法从“int”转换为“int &&”;无法将左值绑定到右值引用// 右值引用可以引用move以后的左值int&& r3 = std::move(a);return 0;
}

二、右值引用的使用场景

左值引用既可以引用左值和又可以引用右值(const左值引用),那为什么C++11还要提出右值引用呢?

  • 左值引用做函数参数可以减少拷贝次数,提高效率。但有时我们需要区分函数参数到底是左值引用还是右值引用。这是const左值引用无法做到的。

  • C++11提出右值引用之后,我们就可以重载一个参数是右值引用的函数,与左值引用进行区分处理。

  • const左值引用可以引用左值和也可以引用右值,但如果有专门的右值引用函数,编译器会优先选择后者。

区分出是右值引用后要干什么呢?

  • 之前我们只有左值引用,由于无法区分引用对象是左值还是右值,所以如果函数内涉及到对复杂类型的拷贝工作时,我们只能统统进行深拷贝,效率底下。
  • 而实际上右值对象通常都是一些匿名对象、函数返回值、表达式返回值等临时对象。其内部资源会在完成拷贝工作后立即被销毁。所以对右值复杂对象的深拷贝其实是一种浪费。
  • 右值引用的应用场景主要是移动语义。简单来说,移动语义就是将原本对右值复杂类型(涉及资源申请)的深拷贝工作,转变为直接移动其内部资源(主要指动态内存)。 移动语义的实现,减少了拷贝次数,提高了程序效率。
  • 移动语义包括:移动构造,移动赋值,移动插入。同时移动构造和移动赋值的实现解决了函数传值返回的深拷贝问题。

2.1 移动构造

以之前模拟实现的string类为例:【STL】模拟实现string类-CSDN博客

在拷贝构造的过程中:

  • 如果拷贝对象是左值,则必须进行深拷贝。

  • 但如果拷贝对象是右值,可以进行移动构造,提高效率。因为右值对象(又叫将亡值)会在完成构造后自动销毁,所以我们可以将右值对象的资源直接拿来占用,免去了开空间和拷贝数据的工作。

class string{
private:char *_str = nullptr; //注意!一定要将指针初始化为nullptr,防止野指针错误。size_t _size = 0;size_t _capacity = 0;public://拷贝构造Mystring(const Mystring &str){ //左值引用_size = str._size;_capacity = str._capacity;_str = new char[_capacity+1];memcpy(_str, str._str, str._size+1);}  //移动构造Mystring(Mystring &&str){ //右值引用swap(str);}  void swap(Mystring &str){::swap(_str, str._str); ::swap(_size, str._size);::swap(_capacity, str._capacity);}
};int main(){string str1 = "abc"; //构造(隐式类型转换)string str2 = str1; //左值构造——拷贝构造string str3 = str1 + str2; //右值构造——移动构造return0;  
}

提示:

  • 注意!一定要将指针初始化为nullptr,防止野指针错误。

  • 在拷贝构造中,左值引用加const,只是为了保证对象在拷贝过程中不被修改。

  • 在移动构造中,右值引用不能加const,因为要在构造时移动右值对象的内部资源。


2.2 移动赋值

移动赋值也是同样的道理:

  • 如果拷贝对象是左值,则必须进行深拷贝。

  • 但如果拷贝对象是右值,可以进行移动赋值。因为右值对象(又叫将亡值)会在完成赋值操作后自动销毁,所以我们可以将右值对象的资源直接拿来占用,同时将赋值对象的原数据交换给右值对象让其帮助销毁。

class string{//拷贝赋值Mystring& operator=(const Mystring &str){ //左值引用if(this != &str) {char *tmp = new char[str._capacity+1]; memcpy(tmp, str._str, str._size+1);delete[] _str; _str = tmp;_size = str._size;_capacity = str._capacity;}return *this; }//移动赋值Mystring& operator=(Mystring &&str){ //右值引用 swap(str);return *this; }
};int main(){string str1 = "abc";string str2 = "def";str1 = str2; //左值赋值——拷贝赋值str1 = "ghi"; //右值赋值——移动赋值    
}

如果想让左值进行移动构造或者移动赋值怎么办?用move()!

在这里插入图片描述

move是一个函数模版,返回指定对象的右值引用,用于将左值临时转换为右值

int main(){string str1 = "abc"; string str2 = str1; //左值构造——拷贝构造string str3 = move(str1); //move将str1临时转为右值——移动构造。//完成移动构造之后,str1中的资源就被转移走了,此时str1为空。return 0}

2.3 移动插入

C++11以后,STL中的所有容器都增加了移动插入接口。

  • 原来C++98中的插入接口其实都是拷贝插入,即不管要插入的元素是左值还是右值都统统需要重新开空间并进行数据拷贝。

  • 而C++11中的移动插入接口则不同,如果插入的元素是右值,则直接移动其资源,无需进行拷贝,提高效率。

在这里插入图片描述

以list为例:

int main(){list<string> ls;string str = "hello world!";ls.push_back(str); //插入左值——拷贝插入ls.push_back(move(str)); //move将左值临时转为右值——移动插入ls.push_back("china"); //插入右值——移动插入
}

提示:list移动插入的模拟实现在【完美转发的使用场景】部分介绍。


2.4 解决传值返回的深拷贝问题

首先在讲解这个问题的解决方法之前,我们需要先回顾一下编译器是如何优化连续的构造和拷贝构造的:

【Object-Oriented C++】类和对象(下) {初始化列表,explicit关键字,匿名对象,static成员,友元,内部类,优化连续的构造和拷贝构造}_芥末虾的博客-CSDN博客

  • 在一条语句中,连续的构造和拷贝构造一般都会被编译器优化,将两个过程合二为一
  • 但是编译器不会对连续的拷贝构造和赋值重载进行优化,不能将两个过程合二为一。

因此当函数传值返回时,构造接收返回值和赋值接收返回值的优化结果是不同的,因该一分为二的看待。

2.4.1 构造接收

如果函数的返回值是一个局部对象,出了函数作用域就会被销毁,就不能使用引用返回,只能传值返回。

例如:在bit::string to_string(int value)函数中可以看到,这里只能使用传值返回。传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造),效率较低。

在这里插入图片描述

但C++11引入了右值引用之后,传值返回的深拷贝问题得到了彻底的解决。

在bit::string类中增加移动构造函数,再去调用bit::to_string(1234)

在这里插入图片描述

  1. 编译器会在返回对象进行销毁之前(调用析构函数),先将其临时转换为右值(类似于move());
  2. 然后调用移动构造,将返回对象中的资源直接移动到接收对象中,完成接收对象的构造。
  3. 最后才销毁返回对象,释放空间 。

注意:

  • 编译器在优化传值返回时,对析构函数的调用顺序做了特殊调整。

  • 不能显示的返回局部对象的右值引用。如果是显示返回,会先析构,再返回。在函数外访问时,空间已经被销毁。


2.4.2 赋值接收

再在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动赋值。

注意:编译器不会对连续的拷贝构造和赋值重载进行优化,不能将两个过程合二为一。

在这里插入图片描述

  1. 这里运行后,我们会发现调用了一次移动构造和一次移动赋值。
  2. 因为如果是用一个已经存在的对象赋值接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象。
  3. 但是我们可以看到,编译器很聪明的在这里把str转换成了右值,调用移动构造来构造临时对象。
  4. 然后再把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的是移动赋值。

总的来说,不管是构造接收还是赋值接收,不管会不会进行合并优化。由于移动构造和移动赋值的实现,使得复杂函数的传值返回不再需要进行深拷贝,大大提高了传值返回的效率。因此,STL中几乎所有的容器都增加了移动构造和移动赋值。


2.5 总结

左值引用和右值引用都是通过减少拷贝来提高效率的。

  • 左值引用:
    • 左值引用传参,左值引用返回,可以直接减少拷贝。
    • 漏洞一:没有解决用右值(将亡值)进行构造、赋值、插入时的对象拷贝问题。
    • 漏洞二:没有解决局部对象传值返回的深拷贝问题。
  • 右值引用:
    • 对于内置类型和没有动态内存申请的复杂类型,移动语义没有什么意义。
    • 但是对于有动态内存申请的复杂类型,移动语义可以间接减少拷贝构造(针对左值引用的漏洞进行了补充)。
    • 补丁一:如果是右值引用传参,则构造、赋值、插入不再进行深拷贝,而是直接移动资源,提高效率。
    • 补丁二:如果有资源申请的复杂类型实现了移动构造和移动赋值,在函数中返回该类型的局部对象时,会将其资源直接移动到外部接收对象中(赋值接收需要移动两次),无需进行拷贝,提高效率。

三、完美转发

3.1 完美转发的概念

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值:

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }//模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
template<typename T>
void PerfectForward(T&& t) //模板中的&&——万能引用
{Fun(t);
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

运行结果:

在这里插入图片描述

为什么全都调用的是左值引用版本的Fun函数呢?

给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,并对值进行修改

  • 例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。

  • 如果不想rr1被修改,可以用const int&& rr1 去引用,const右值引用可以取地址,不可修改。

  • 可以这么理解:右值取右值引用后变为了左值,这么设计是因为要使用右值引用移动右值对象的资源,而移动资源就意味着要修改右值(矛盾),所以要将右值转为左值。

int main()
{double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20;rr2 = 5.5; // 报错,const右值引用不能修改return 0;
}

那么如何在内外层函数传递参数的过程中保持参数的原生类型属性呢?这时就需要用到新语法:完美转发

//同样还是上面的代码,加入完美转发
template<typename T>
void PerfectForward(T&& t) //模板中的&&——万能引用
{// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。Fun(std::forward<T>(t));
}

再次运行:

在这里插入图片描述

注意:在多层嵌套调用时,要想在内外层函数传递参数的过程中保持参数的原生类型属性,需要在所有的传参位置进行完美转发。


3.2 完美转发的使用场景

以之前模拟实现的list和string为例:

  • 【STL】模拟实现list类模版 {深度剖析list迭代器,实现list深拷贝}_芥末虾的博客-CSDN博客

  • 【STL】模拟实现string类-CSDN博客(已实现移动构造)

下面我们实现Mylist的移动插入:

template <class T>    
struct list_node{T _data;                         list_node *_next;        list_node *_prev;    //节点的构造 list_node(const T &val = T()) //左值引用    :_data(val), //调用存储类型的拷贝构造             _next(nullptr),    _prev(nullptr)    {}  //重载了右值引用版本                         list_node(T &&val = T()) //右值应用    :_data(forward<T>(val)), //完美转发3-->调用存储类型的移动构造  _next(nullptr),    _prev(nullptr)                 {}                         
};                             template <class T>
class List{//拷贝插入 iterator insert(iterator pos, const T &val){Node *cur = pos._pnode;Node *prev = cur->_prev;Node *newnode = new Node(val);prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}//移动插入  iterator insert(iterator pos, T &&val){Node *cur = pos._pnode;Node *prev = cur->_prev;//需要在所有的传参位置进行完美转发Node *newnode = new Node(forward<T>(val)); //完美转发2prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}//拷贝插入 void push_back(const T &val){  insert(end(), val); //val是左值引用,调用拷贝插入insert             }                                //移动插入                     void push_back(T &&val){               //insert(end(), val); //右值引用将val转换为左值,所以也调用拷贝插入insert  insert(end(), forward<T>(val)); //完美转发1}    
};

测试代码:

#include <iostream>    
#include "list.hpp"    
#include "string.hpp"    
using namespace std;    int main(){        Mylist<Mystring> ls; //在创建头结点时会进行一次移动构造(用匿名对象初始化头结点)cout << "----------------------------------" << endl;    Mystring str1 = "abcd";    cout << "----------------------------------" << endl;    ls.push_back(str1); //插入左值——拷贝构造                                   cout << "----------------------------------" << endl;      ls.push_back(Mystring("qwer"));  //插入右值——移动构造        cout << "----------------------------------" << endl;      ls.push_back("1234"); //插入右值——移动构造                 cout << "----------------------------------" << endl;      
}             

完美转发前:

在这里插入图片描述

由于右值引用会将右值的属性转换为左值,所以也去调用了拷贝插入insert 。因此我们需要将移动插入过程中所有涉及的函数都实现一份右值引用版本,并在所有的传参位置进行完美转发,以保持参数的右值属性。

完美转发后:

在这里插入图片描述

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

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

相关文章

【笔记】Splay 详细解读

【笔记】Splay 目录 简介右旋左旋 核心思想操作a. Splayb. 插入c. 删除 信息的维护例题AcWing 2437. SplayP3369 【模板】普通平衡树 简介 Splay 是一种平衡树&#xff0c;并且是一棵二叉搜索树&#xff08;BST&#xff09;。 它满足对于任意节点&#xff0c;都有左子树上任意…

Firefox 开发团队对 Vue 3 进行优化效果显著

Mozilla 官方博客近日发表文章《Faster Vue.js Execution in Firefox》&#xff0c;介绍了 Firefox 开发团队对 Vue 3 进行的优化。 文章写道&#xff0c;在使用 Speedometer 3 对 Firefox 进行基准测试时&#xff0c;他们发现 Vue.js test 的测试结果从 Vue 2 升级到 Vue 3 后…

unity 限制 相机移动 区域(无需碰撞检测)

限制功能原著地址&#xff1a;unity限制相机可移动区域&#xff08;box collider&#xff09;_unity限制相机移动区域_manson-liao的博客-CSDN博客 一、创建限制区域 创建一个Cube&#xff0c;Scale大小1&#xff0c;添加组件&#xff1a;BoxCollder&#xff0c;调整BoxColld…

YOLOV8-DET转ONNX和RKNN

目录 1. 前言 2.环境配置 (1) RK3588开发板Python环境 (2) PC转onnx和rknn的环境 3.PT模型转onnx 4. ONNX模型转RKNN 6.测试结果 1. 前言 yolov8就不介绍了&#xff0c;详细的请见YOLOV8详细对比&#xff0c;本文章注重实际的使用&#xff0c;从拿到yolov8的pt检测模型&…

GitHub上有助于开发微信小程序的仓库

2023年9月30日&#xff0c;周六晚上 最近帮同学在GitHub找了一些开发小程序会用到的东西 目录 UI库WePY框架基于WePY框架的Demo微信小程序开发资源汇总 UI库 GitHub - Tencent/weui-wxss: A UI library by WeChat official design team, includes the most useful widgets/m…

CSS详细基础(二)文本样式

插播一条CSS的工作原理&#xff1a; CSS是一种定义样式结构如字体、颜色、位置等的语言&#xff0c;被用于描述网页上的信息格式化和显示的方式。CSS样式可以直接存储于HTML网页或者单独的样式单文件。无论哪一种方式&#xff0c;样式单包含将样式应用到指定类型的元素的规则。…

数据结构-----二叉排序树

目录 前言 1.什么是二叉排序树 2.如何构建二叉排序树 3.二叉排序树的操作 3.1定义节点储存方式 3.2插入节点操作 3.2创建二叉排序树 3.4遍历输出&#xff08;中序遍历&#xff09; 3.5数据查找操作 3.6获取最大值和最小值 3.7删除节点操作 3.8销毁二叉排序树 4.完…

【文献】TOF标定 Time-of-Flight Sensor Calibration for a Color and Depth Camera Pair

文章目录 Article info.Introduction处理TOF误差Take home messagesResourcesIDEAS Article info. Time-of-Flight Sensor Calibration for a Color and Depth Camera Pair IEEE TRANSACTIONS ON PATTERN ANALYSIS AND MACHINE INTELLIGENCE, VOL. 37, NO. 7, JULY 2015 Intr…

nextTick源码解读

&#x1f4dd;个人主页&#xff1a;爱吃炫迈 &#x1f48c;系列专栏&#xff1a;Vue &#x1f9d1;‍&#x1f4bb;座右铭&#xff1a;道阻且长&#xff0c;行则将至&#x1f497; 文章目录 nextTick原理nextTicktimerFuncflushCallbacks 异步更新流程updatequeueWatcherflushS…

ROS2 库包设置和使用 Catch2 进行单元测试

说明 本文的目的是了解如何在 ROS2 中创建库&#xff0c;以供其他 ROS2 包使用。除此之外&#xff0c;本文还介绍了如何使用 catch2 框架编写单元测试。本文的第 1 部分将详细介绍如何创建库包。第 2 部分将介绍 ROS2 软件包如何利用创建的库 上篇 ROS2 库包设置和使用 Catch2…

GEO生信数据挖掘(一)数据集下载和初步观察

检索到目标数据集后&#xff0c;开始数据挖掘&#xff0c;本文以阿尔兹海默症数据集GSE1297为例 目录 GEOquery 简介 安装并加载GEOquery包 getGEO函数获取数据&#xff08;联网下载&#xff09; 更换下载数据源 对数据集进行初步观察处理 GEOquery 简介 GEOquery是一个…

【AntDesign】封装全局异常处理-全局拦截器

[toc] 场景 本文前端用的是阿里的Ant-Design框架&#xff0c;其他框架也有全局拦截器&#xff0c;思路是相同&#xff0c;具体实现自行百度下吧 因为每次都需要调接口&#xff0c;都需要单独处理异常情况&#xff08;code !0&#xff09;&#xff0c;因此前端需要对后端返回的…

联邦学习-Tensorflow实现联邦模型AlexNet on CIFAR-10

目录 Client端 Server端 扩展 Client.py Server.py Dataset.py Model.py 分享一种实现联邦学习的方法&#xff0c;它具有以下优点&#xff1a; 不需要读写文件来保存、切换Client模型 不需要在每次epoch重新初始化Client变量 内存占用尽可能小&#xff08;参数量仅翻一…

1.4.C++项目:仿muduo库实现并发服务器之buffer模块的设计

项目完整版在&#xff1a; 一、buffer模块&#xff1a; 缓冲区模块 Buffer模块是一个缓冲区模块&#xff0c;用于实现通信中用户态的接收缓冲区和发送缓冲区功能。 二、提供的功能 存储数据&#xff0c;取出数据 三、实现思想 1.实现换出去得有一块内存空间&#xff0c;采…

Learning Invariant Representation for Unsupervised Image Restoration

Learning Invariant Representation for Unsupervised Image Restoration (Paper reading) Wenchao Du, Sichuan University, CVPR20, Cited:63, Code, Paper 1. 前言 近年来&#xff0c;跨域传输被应用于无监督图像恢复任务中。但是&#xff0c;直接应用已有的框架&#xf…

【python海洋专题三】图像修饰之画布和坐标轴

【python海洋专题三】图像修饰之画布和坐标轴 海洋与大气科学 上期读取nc水深文件&#xff0c;并出图 但是存在一些不完美&#xff0c;本期修饰 本期内容目录 1&#xff1a;改变画布大小 2&#xff1a;改变画布背景色 3&#xff1a;改变画布在显示屏中的显示位置 4&#xf…

【项目管理】--敏捷开发管理之Scrum

目录 一、前言二、what---敏捷开发是什么2.1、敏捷开发宣言2.2、敏捷开发原则2.3、一句话概述敏捷开发三、why---为什么会有敏捷开发3.1、传统开发模式和敏捷开发模式对比四、how---敏捷开发怎么实践到项目团队4.1、what---Scrum是什么4.2、what---Scrum有哪些内容(1)、Scrum之…

NLP 01(介绍)

一、NLP 自然语言处理 (Natural Language rrocessing,简称NLP) 是计算机科学与语言学中关注于计算机与人类语言间转换的领域。 1.1 发展 规则&#xff1a;基于语法 自然语言处理的应用场景: 语音助手 机器翻译 搜索引擎 智能问答

【单片机】12-串口通信和RS485

1.通信有关的常见概念 区分&#xff1a;串口&#xff0c;COM口&#xff0c;UART&#xff0c;USART_usart和串口区别-CSDN博客 串口、COM口、UART口, TTL、RS-232、RS-485区别详解-CSDN博客 1.什么是通信 &#xff08;1&#xff09;人和人之间的通信&#xff1a;说话&#xff…