深入理解右值引用与移动语义

文章目录

    • 写在前面
    • 1. 什么是右值,什么是左值?
      • 1.1右值引用可以引用左值吗
      • 1.2 左值引用、右值引用本身是左值还是右值?
      • 1.3 特殊的 const 左值引用
    • 2. 右值引用与移动构造的意义
    • 3. 移动构造函数的使用
    • 4. move的实现原理
    • 5. 完美转发

写在前面

本文主要为大家梳理以下几个问题:

  • 什么是右值
  • 右值引用的意义与使用场景
  • std::move 函数的本质
  • 如何编写移动构造函数
  • 万能引用与完美转发

参考资料:

  1. 《一文读懂C++右值引用与std::move》

  2. 《C++高阶知识:深入分析移动构造函数及其原理》

  3. 《Value categories》

  4. 《C++11的 value category 以及 move semantics》

由于作者才疏学浅,理解欠缺的地方欢迎大家指正


1. 什么是右值,什么是左值?

 每个 C++ 表达式(包括操作符和其操作数、字面值、变量名等)具有两个独立的属性:类型和值类别
 类型(type)大家都不陌生,指的是表达式的数据类型,它定义了表达式的取值范围和可执行的操作。例如,一个整数表达式的类型可以是 int,一个浮点数表达式的类型可以是 float。
 值类别(value category)可以理解为表达式的身份和可移动性。根据 C++ 标准,有三种最主要的值类别:右值(rvalue)、左值(lvalue)、将亡值(xvalue),他们三者的关系如下:
在这里插入图片描述

  • 身份决定了它是否具有表达式寻址性,即我们是否可以获取其在内存中的地址
  • 可移动性如果出现在赋值,初始化等语句中,是否会使语句呈现移动语义

上面说的有些抽象,我们结合具体的例子来分析,根据表现出的特征进行区分:

[右值与右值引用]:

// 以下都是常见的右值
10;
x + y;
fmin(x, y);// 右值引用
int&& rr1 = 10;
double&& rr2 = x + y;             
double&& rr3 = fmin(x, y); 		  
  • 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等
  • 右值不能放在赋值符号的左边,不信你试试 10 = x + y
  • 对右值的引用就是右值引用,用 && 表示
  • 不能对右值取地址,不信你试试 int* p = &10;

[左值与左值引用]:

// 以下a、b、pa都是左值
int a = 10;    
int b = a;
int* pa = &a; // 左值引用
int& rla = a;
  • 左值可以出现在赋值符号的左边和右边
  • 可以取它的地址,可以为他赋值

下面来考虑一些疑难问题:

1.1右值引用可以引用左值吗

可以。 std::move() 函数可以将左值强转为右值。没错,就是强制类型转换,我们后面将结合源码具体谈到:

int a = 10;
int&& rr = std::move(a);

1.2 左值引用、右值引用本身是左值还是右值?

 被声明出来的左、右值引用都是左值。因为根据C++语言规范,无论是左值引用还是右值引用,它们都被认为是具名对象,具有地址并且可以寻址。因此,在使用引用时,它们被视为左值。用下面的代码验证:

void check(int&& rr) {cout << "Yes" << endl;
}int main() {int a = 5;	       		 // a是个左值int& ref_a_left = a;	   	// ref_a_left是个左值引用,本身是左值int&& ref_a_right = std::move(a); // ref_a_right是个右值引用,本身是左值check(a);              // 编译不过,无法将左值绑定到右值check(ref_a_left);     // 编译不过,左值引用ref_a_left本身也是个左值check(ref_a_right);    // 编译不过,右值引用ref_a_right本身也是个左值check(std::move(a));			// 编译通过check(std::move(ref_a_right));  // 编译通过check(std::move(ref_a_left));   // 编译通过
}

 右值引用本身是一个左值,那么也就不难理解,为什么右值不可以修改,但是右值引用可以修改:我们并不是修改右值,而是修改右值引用所引用的对象;右值引用是左值,它有自己的标识符和地址。

int && rr = 10;
rr = 20;

1.3 特殊的 const 左值引用

 const 左值引用比较特殊,它既可以接受左值,也可以接受右值。和右值引用一样, const 左值引用能够延长右值的生命周期,以避免产生悬空引用。下面是cppreference 中的说明:

在这里插入图片描述

 当我们将一个右值绑定到 const 左值引用上时,编译器会自动创建一个临时对象,并将该右值绑定到这个临时对象上。这个临时对象的生命周期会与 const 左值引用的声明周期相同,从而确保了在 const 左值引用的作用域内能够安全地使用这个右值。
 这也是为什么要使用 const & 作为函数参数的原因之一。如果没有const,这样的代码就无法编译通过了:v.push_back(5)

void push_back (const value_type& val);

2. 右值引用与移动构造的意义

 左值引用做参数和返回值都可以提高效率。但是左值引用的短板在于,如果引用的对象出作用域销毁,那么就不能使用左值引用了。例如下面的例子中(不考虑编译器优化),hello()函数在返回时,首先会将 "hello world" 拷贝给临时变量。
  这个临时变量本质上属于将亡值,具有身份的同时又具有移动性。在没有移动构造前,s只能把临时变量的内容拷贝复制一份,而眼睁睁的看着临时变量出作用域销毁 —— 白白浪费!

string hello() {return "hello world";
}
string s = hello();

 但是对于一个这个即将被销毁的对象,我们为什么不聪明点,直接将其中的资源占为已有呢?将对方资源所有权转移过来,这就是移动构造的核心思想。如何转移?其实就是指针做一个指针交换:

namespace my{string(string&& s):_str(nullptr) ,_size(0), _capacity(0){swap(str_, s.str_)        // 所有权转移 }
};

 注意,移动构造后一定要将原对象中的指针置为空,否则一块空间会被 delete 两次。而对于 nullptr, delete 多次也没有影响。

 理解了移动构造的价值后,我们进一步想,哪些资源可以被移动构造?C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。

 你应该也认同了只有右值才适合移动,那我们如何显示的接收一个右值呢?在C++11之前,只有 const 修饰的左值引用才能接收右值,这可是个大问题啊,你都被const修饰了,我还怎么“偷”你的资源呢?顺着这个逻辑思考,右值引用的出现也是一个必然,它为移动构造的出现洒下了肥沃的土壤。

 接下来我们来手动实现下移动构造函数(移动赋值运算符同理)

3. 移动构造函数的使用

namespace my {class string {public:string() : len_(0), cap_(0), data_(nullptr) {}string(const char* s) {// 略}string(const my::string& s) {cout << "拷贝构造函数" << endl;// 略}my::string& operator=(const my::string& s) noexcept {cout << "赋值运算符重载" << endl;// 略}string(my::string&& s) {cout << "移动构造函数" << endl;len_ = s.len_;cap_ = s.cap_;swap(data_, s.data_);}my::string& operator=(my::string&& s) noexcept{cout << "移动赋值运算符重载" << endl;len_ = s.len_;cap_ = s.cap_;if (data_) {delete data_;data_ = nullptr;}swap(data_, s.data_);return *this;}private:char* data_;int len_;int cap_;};
};

STL库中基本都支持了移动构造和移动赋值,例如string等等

string (string&& str) noexcept;
string& operator= (string&& str) noexcept;

swap函数也是
在这里插入图片描述
 给大家分享一个我初学时容易犯的错误:我们仍然沿用上面实现的 my::string类做测试,大家觉得 s = t.s 中有没有调用移动赋值呢?

class test {public:test() {}// ……test(test&& t) {s = t.s;}private:my::string s;};

在这里插入图片描述
答案是并没有。虽然t是一个右值,但是t.m确实是一个左值:
在这里插入图片描述

4. move的实现原理

 刚开始学习 std::move,大家总是容易对move函数抱有误解,认为move函数完成了内存上资源的移动,然而实际上move完成的工作只是强制类型转换,我们来看看相应的源码:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{return static_case<typename remove_reference<T>::type&&>(t);
}

 虽然细节上我们并不了解,但我们大致可以看出 move 就是完成了强制类型转换的工作。为了更加透彻的理解,我会为大家说明其中的细节

  • 在模板中,&& 并不是代表右值引用,而是万能引用。它既可以接收左值,也可以接收右值。

  • 这个返回值 typename remove_reference<T>::type&& 是什么意思呢?type是定义在 remove_reference 中的类型成员,因此访问它时也与访问静态成员一样用::访问,而该类是一个模板类,所以在它前面要加typename关键字。

  • remove_reference 从它的名字也可以看出,它是通过模板去除引用
    在这里插入图片描述

  • 我们假定T为int&,即传入左值,那么最后可以将上面的代码简化成如下的形式:

    int && move(int& && t){return static_case<int&&>(t);
    }
    
  • 遇到 int& && 的时候,会发生引用折叠,折叠的规则如下图所示:
    在这里插入图片描述

  • 所以最终move其实就做了这么一件事:

    int && move(int& t){return static_case<int&&>(t);
    }
    

5. 完美转发

 我们前面谈到,在模板中,&& 既可以接收左值,也可以接收右值,但是当我们在函数内部,将val 传递给另一个函数的时候,val将发生退化此时,val总是是被当作左值进行传递的。

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 = 10;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值
}

在这里插入图片描述

  • 为了保持参数原有的左右值属性,我们需要使用std::forward<模板参数>()函数来实现完美转发:
    在这里插入图片描述

  • 注意!为了保持参数原有的左右值属性,所有的向下转发都需要实现完美转发:

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

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

相关文章

rust String 和 str 区别

1 String / &String String 类型的变量本质是一个存放在栈上的胖指针&#xff08;当然调用过程中&#xff0c;不用显示地按指针那样处理&#xff09;&#xff0c;共有三个字段&#xff1a; 1 pointer: 指向实际字符串值的地址&#xff0c;值是存放在堆上可变字节缓冲区&a…

焊接符号学习

欧美焊接符号举例 4.5------表示焊点直径 【3】------根据图示说明&#xff0c;表示此项为CC项或者SC项 6-------表示此处为第六CC项或者SC项 BETWEEN①AND②------表示①件和②件俩点之间的焊点 12X------表示俩点之间的焊点个数为12个 日本焊接符号举例 A------根据图示&…

JDK10特性

文章目录 JAVA10概述语法层次的变化局部变量的类型推断不能使用类型推断的场景变量的声明初始值nulllambda表达式方法引用为数组静态初始化成员变量不能使用其他不可以的场景 API层次的变化集合的copyOf方法 总结 JAVA10概述 2018年3月21日&#xff0c;Oracle官方宣布JAVA10正…

OSI模型与数据的封装

1、OSI模型 上层|| 七层模型 四层模型|| 应用层| 表示层 应用层 http/ftp/ssh/ftps| 会话层 -----------------------------------------------------------------------| 传输层 传输层 tcp/udp ------------------------------…

解决:Loading class `com.mysql.jdbc.Driver‘. This is deprecated.

1.在连接MySQL数据库时候会出现这个报错 Loading class com.mysql.jdbc.Driver. This is deprecated. The new driver class is com.mysql.cj.jdbc.Driver. The driver is automatically registered via the SPI and manual loading of the driver class is generally unneces…

gitlab操作

1. 配置ssh 点击访问 2. 创建新分支与切换新分支 git branch 新分支名 // 创建 git checkout 新分支名 // 切换到新分支3. 查看当前分支 git branch*所指的就是当前所在分支 4. 本地删除文件后与远程git同步 git add -A git commit -m "del" git push

C++项目实战——基于多设计模式下的同步异步日志系统-⑧-日志落地类设计

文章目录 专栏导读抽象基类StdoutSink类设计FileSink类设计RollBySizeSink类设计日志落地工厂类设计日志落地类整理日志落地拓展测试RollByTimeSink类设计测试代码测试完整代码 专栏导读 &#x1f338;作者简介&#xff1a;花想云 &#xff0c;在读本科生一枚&#xff0c;C/C领…

民用大中型无人直升机系统飞行性能飞行试验要求

声明 本文是学习GB-T 42856-2023 民用大中型无人直升机系统飞行性能飞行试验要求. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件规定了民用大中型无人直升机系统飞行性能飞行试验的内容、目的、条件、实施、数据处理和 结果评定等要…

线性代数的本质(三)——线性方程组

文章目录 线性方程组高斯消元法初等行变换线性方程组的解向量方程齐次线性方程组的解非齐次线性方程组的解 线性方程组 高斯消元法 客观世界最简单的数量关系是均匀变化的关系。在均匀变化问题中&#xff0c;列出的方程组是一次方程组&#xff0c;我们称之为线性方程组(Linea…

第6章_瑞萨MCU零基础入门系列教程之串行通信接口(SCI)

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

稀土系储氢合金 吸放氢反应动力学性能测试方法

声明 本文是学习GB-T 42656-2023 稀土系储氢合金 吸放氢反应动力学性能测试方法. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件描述了稀土系储氢合金吸/放氢反应动力学性能的测试方法。 本文件适用于采用体积法进行稀土系储氢合金的…

RabbitMQ基础概念-02

RabbitMQ是基于AMQP协议开发的一个MQ产品&#xff0c; 首先我们以Web管理页面为 入口&#xff0c;来了解下RabbitMQ的一些基础概念&#xff0c;这样我们后续才好针对这些基础概念 进行编程实战。 可以参照下图来理解RabbitMQ当中的基础概念&#xff1a; 虚拟主机 virtual hos…