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

news2024/10/6 16:26:35

文章目录

    • 写在前面
    • 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.coloradmin.cn/o/1015426.html

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

相关文章

Ocoya:快速创建社交媒体上的文字

【产品介绍】 名称 Ocoya 具体描述 Ocoya是一个人工智能文字创建平台&#xff0c;速度能提高10倍&#xff0c;节省高达80%的时间&#xff0c;在几分钟内完成内容营销、文案写作和社交媒体&#xff01;可用于创建和安排社交媒体内容&#xff0c;减轻你的团队负担。 【团队介绍】…

KMP算法(C++)

KMP算法与BF算法不一样的在于&#xff0c;当主串与子串不匹配时&#xff0c;主串不回溯&#xff0c;选择了子串回溯&#xff0c;大大提高了运算效率。 借用了next1【】数组&#xff0c;让子串回溯。get_next函数求next1【】数组&#xff0c;get_next函数的实现难点在于下列几行…

数据库开发-MySQL基础DQL和多表设计

1. 数据库操作-DQL DQL英文全称是Data Query Language(数据查询语言)&#xff0c;用来查询数据库表中的记录。 1.1 介绍 查询关键字&#xff1a;SELECT 查询操作是所有SQL语句当中最为常见&#xff0c;也是最为重要的操作。在一个正常的业务系统中&#xff0c;查询操作的使…

MC-4/11/01/400 ELAU 软件允许用户完全访问相机设置

MC-4/11/01/400 ELAU 软件允许用户完全访问相机设置 一个完整的Sentinel模具保护解决方案包括一到四台冲击式摄像机、专用红外LED照明和镜头、Sentinel软件以及所有与模压机连接的必要互连组件。摄像机支架基于磁性&#xff0c;可快速、安全、灵活地部署。此外&#xff0c;一个…

Kotlin Android中错误及异常处理最佳实践

Kotlin Android中错误及异常处理最佳实践 Kotlin在Android开发中的错误处理机制以及其优势 Kotlin具有强大的错误处理功能&#xff1a;Kotlin提供了强大的错误处理功能&#xff0c;使处理错误变得简洁而直接。这个特性帮助开发人员快速识别和解决错误&#xff0c;减少了调试代…

在SpringSecurity + SpringSession项目中如何实现当前在线用户的查询、剔除登录用户等操作

1、前言 在前一篇《在SpringBoot项目中整合SpringSession&#xff0c;基于Redis实现对Session的管理和事件监听》笔记中&#xff0c;已经实践了在SpringBoot SpringSecurity 项目中整合SpringSession&#xff0c;这里我们继续尝试如何统计当前在线用户&#xff0c;思路如下&am…

看好多人都在劝退学计算机,可是张雪峰又 推荐过计算机,所以计算机到底是什么样 的?

张雪峰高考四百多分&#xff0c;但是他现在就瞧不起400多分的学生。说难听点&#xff0c;六七百分的 热门专业随便报谁不会啊&#xff1f; 计算机专业全世界都是过剩的&#xff0c;今年桂林电子科技&#xff0c;以前还是华为的校招大学&#xff0c;今年 计算机2/3待业。这个世…

程序员兼职社区招募(内含技术指导)

&#x1f468;‍&#x1f4bb;作者简介&#xff1a;大数据专业硕士在读&#xff0c;CSDN人工智能领域博客专家&#xff0c;阿里云专家博主&#xff0c;专注大数据与人工智能知识分享。公众号&#xff1a; GoAI的学习小屋&#xff0c;免费分享书籍、简历、导图等资料&#xff0c…

flex:1的大坑

一、问题描述 整个类名为roomList 的大盒子设置了flex为1&#xff0c;与它同级的其他盒子都已经设置了宽高&#xff0c;但roomList 依然被内容撑开了&#xff0c;没有自适应 .roomList { flex: 1; } 二、原因分析 roomList的整个高度溢出&#xff0c;对于包裹roomList的父盒子…

pycharm安装(windows)

一、下载及安装 1.下载进入PyCharm官方下载地址&#xff1a; https://www.jetbrains.com/pycharm/download/ 下拉一下&#xff0c;直接下载社区版就行&#xff0c;是免费的&#xff0c;功能足够用了。 2.安装 (1) 找到你下载PyCharm的路径&#xff0c;双击.exe文件进行安装…

每日一题~合并二叉树

题目链接&#xff1a;617. 合并二叉树 - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 思路分析&#xff1a; 由图可知&#xff0c;当两个位置都有节点的时候&#xff0c;直接将两个节点的 val 相加就是结果&#xff0c;如果在一个位置两棵树只有一棵在此位置上…

Vim的基础操作

前言 本文将向您介绍关于vim的基础操作 基础操作 在讲配置之前&#xff0c;我们可以新建一个文件 .vimrc&#xff0c;并用vim打开在里面输入set nu 先给界面加上行数&#xff0c;然后shift &#xff1b;输入wq退出 默认打开&#xff1a;命令模式 在命令模式中&#xff1a…

06乐观锁与悲观锁

乐观锁与悲观锁 悲观锁: 悲观锁比较适合插入数据,简单粗暴但是性能一般 乐观锁: 比较适合更新数据, 性能好但是成功率低(多个线程同时执行时只有一个可以执行成功),还需要访问数据库造成数据库压力过大 模拟乐观锁实现流程 第一步: 数据库中增加商品表t_product并插入一条数…

【MySQL】基础SQL语句——表的操作

文章目录 一. 创建表二. 查看表结构三. 修改表3.1 修改表名或列名3.2 插入数据3.3 添加列3.4 修改列类型3.5 删除列 四. 删除表结束语 一. 创建表 create table table_name(field1 datatype,field2 datatype...) charset 字符集 collate 校验规则 engine 存储引擎; 创建表 fiel…

07通用枚举和表的代码生成器

通用枚举 通用枚举 如果表中的有些字段值是固定的例如性别(男或女),此时我们可以使用MyBatis-Plus的通用枚举来为属性赋值 需求: 在数据库表添加字段sex 第一步: 设置枚举类型,使用EnumValue注解将注解所标识的属性值存储到数据库中 // 枚举类型只要设置getter方法 Getter …

2023/09/15 qt day1

代码实现图形化界面 #include "denglu.h" #include "ui_denglu.h" #include <QDebug> #include <QIcon> #include <QLabel> #include <QLineEdit> #include <QPushButton> denglu::denglu(QWidget *parent): QMainWindow(p…

JavaScript-promise使用+状态

Promise 什么是PromisePromise对象就是异步操作的最终完成和失败的结果&#xff1b; Promise的基本使用&#xff1a; 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compati…

层次聚类分析

1、python语言 from scipy.cluster import hierarchy # 导入层次聚类算法 import matplotlib.pylab as plt import numpy as np# 生成示例数据 np.random.seed(0) data np.random.random((20,1))# 使用树状图找到最佳聚类数 Z hierarchy.linkage(data,methodweighted,metric…

时序预测 | MATLAB实现NGO-BiLSTM北方苍鹰算法优化双向长短期记忆网络时间序列预测

时序预测 | MATLAB实现NGO-BiLSTM北方苍鹰算法优化双向长短期记忆网络时间序列预测 目录 时序预测 | MATLAB实现NGO-BiLSTM北方苍鹰算法优化双向长短期记忆网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 时序预测 | MATLAB实现NGO-BiLSTM北方苍鹰算法优…