右值引用和move语义

文章目录

  • 一、左值和右值
  • 二、左值引用和右值引用
  • 三、move语义
  • 四、右值引用的作用
    • 1.左值引用的作用和局限
    • 2.右值引用的作用
    • 3.重谈std::move()
    • 4.完美转发


一、左值和右值

在《C Primer Plus》书中这样提到左值:左值是用于标识或定位存储位置的标签。

对于早期的 C 语言,提到左值意味着:

  1. 它指定一个对象,可以引用内存中的地址
  2. 它可用在赋值运算符的左侧

但是后来,标准中新增了 const 限定符。用 const 创建的变量不能被修改。因此,const 标识符满足上面的第1项,但是不满足第2项。一方面 C 继续把标识对象的表达式定义为左值,一方面某些左值却不能放在赋值运算符的左侧。

为此,C 标准新增了一个术语:可修改的左值,用于标识可修改的对象。所以,赋值运算符的左侧应该是可修改的左值。

右值指的是能够赋值给可修改左值的量,且本身不是左值。

所以我们总结出左值和右值的区别:

  • 左值可以取地址,可以出现在赋值符号的左边或右边
  • 右值不能取地址,不能出现在赋值符号的左边,可以出现在赋值符号的右边

二、左值引用和右值引用

传统的c++引用(现在称为左值引用)使得标识符关联到左值:

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;
	return 0;
}

C++11新增了右值引用,这是用 && 表示的。右值引用可以关联到右值。

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 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。

int main()
{
	 double x = 1.1, y = 2.2;
	 int&& rr1 = 10;
	 const double&& rr2 = x + y;
	 rr1 = 20;
	 rr2 = 5.5;  // 报错
	 return 0;
}

对于二者的比较:

左值引用的总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。
int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值
    
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}

右值引用的总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
int main()
{
	 // 右值引用只能右值,不能引用左值。
	 int&& r1 = 10;
	 
	 // error C2440: “初始化”: 无法从“int”转换为“int &&”
	 // message : 无法将左值绑定到右值引用
	 int a = 10;
	 int&& r2 = a;
	 
	 // 右值引用可以引用move以后的左值
	 int&& r3 = std::move(a);
	 return 0;
}

三、move语义

C++11 之前,只有 copy 语义,这对于极度关注性能的语言而言是一个重大的缺失。那时候程序员为了避免性能损失, 只好采取规避的方式。比如:

std::string str = s1;
str += s2;

这种写法就可以规避不必要的拷贝。而更加直观的写法:

std::string str = s1 + s2;

对于 move 语义的急迫需求,到了 C++11 终于被引入。其直接的驱动力很简单:在构造或者赋值时, 如果等号右侧是一个中间临时对象,应直接将其占用的资源直接 move 过来(对方就没有了)。

但问题是,如何让一个构造函数,或者赋值操作重载函数能够识别出来这是一个临时变量?

在 C++11 之前,拷贝构造和赋值重载的原型如下:

struct Foo {
   Foo(const Foo&);
   Foo& operator=(const Foo&);
};

参数类型都是 const & ,它可以匹配到三种情况:

  1. non-const lvalue reference :非const左值引用
  2. const lvalue reference :const左值引用
  3. const rvalue reference :const右值引用

对于 non-const rvalue reference 是无能为力的。 另外,即便是能捕捉 const rvalue reference , 比如: foo = Foo(10); ,但其 const 修饰也保证了其资源不可能被 move 走。

因而,能够被 move 走资源的,恰恰是之前缺失的那种引用类型: non-const rvalue reference 。

这时候,就需要有一种表示法,明确识别出那是一种 non-const rvalue reference ,最后定下来的表示法是 T&& 。 这样,就可以这样来定义不同方式的构造和赋值操作:

struct Foo {
   Foo(const Foo&);   // copy ctor
   Foo(Foo&&);        // move ctor

   Foo& operator=(const Foo&); // copy assignment
   Foo& operator=(Foo&&);      // move assignment
};

通过这样的方式,让 foo = Foo(10) 这样的表达式,都可以匹配到 move 语义的版本。 与此同时,让 foo = foo1 这样的表达式,依然使用 copy 语义的版本。

四、右值引用的作用

我们使用下面的类作为例子,分析左值引用和右值引用:

namespace tes
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "默认构造 -- string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
		{

			string tmp(s._str);
			swap(tmp);
			cout << "拷贝构造 -- string(const string& s) " << endl;

		}

		// 赋值重载
		string& operator=(string s)
		{
			cout << "赋值重载 -- string& operator=(string s) " << endl;
			//string tmp(s);
			//swap(tmp);
			swap(s);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};

	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		tes::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		std::reverse(str.begin(), str.end());
		return str;
	}
}

1.左值引用的作用和局限

左值引用做参数或返回值都可以减少拷贝,提高效率:

void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
	 tes::string s1("hello world");
	 
	 // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景
	 func1(s1);
	 func2(s1);
	 
	 // string operator+=(char ch) 传值返回存在深拷贝
	 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率

	 return 0;
}

但是左值引用也有它的局限,当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回;当存在中间临时对象时,move 语义要比 copy 语义效率更高。

2.右值引用的作用

我们进行如下调用时会发现to_string()的函数返回值是一个右值,而将这个右值赋值给ret时发生了深拷贝,所以影响了效率:

在这里插入图片描述

我们可以通过右值引用来实现移动拷贝和移动赋值提高效率,移动构造本质是将参数右值的资源窃取过来,占为已有,那么就不用做深拷贝了。移动赋值原理也一样。:

	// 移动构造
string(string&& s)
{
	cout << "string(const string& s) -- 移动拷贝" << endl;
	swap(s);
}

	// 移动赋值
string& operator=(string&& s) noexcept
{
	cout << "string& operator=(string s) -- 移动赋值" << endl;
	swap(s);
	
	return *this;
}

右值就会匹配到移动赋值函数,就直接将资源拿了过来,避免了拷贝,提高了效率。

在这里插入图片描述

我们可以通过下面的内存窗口观察其原理:s2 自己开辟了一块空间然后进行拷贝构造,而 s3 是直接将 s1 的空间抢占过来进行构造(移动构造),从而避免了拷贝。当然,我们不希望别人抢占一个像左值这样生命周期长的资源,这带来的风险与收益不成正比,所以,我们往往希望抢占一个临时的中间变量,而右值往往充当这类角色,这样就可以减少拷贝,提高效率了!

在这里插入图片描述

而在STL容器的接口中,也增加了右值引用的版本:

在这里插入图片描述

3.重谈std::move()

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
	// forward _Arg as movable
 	return ((typename remove_reference<_Ty>::type&&)_Arg);
}

4.完美转发

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。

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;
}

在这里插入图片描述

我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发:

template<typename T>
void PerfectForward(T&& t)
{
	Fun(std::forward<T>(t));
}

通过完美转发,我们可以得到 t 的原生类型属性 :

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Kafka框架详解

Kafka 1、Kafka介绍 ​ Kafka是最初由linkedin公司开发的&#xff0c;使用scala语言编写&#xff0c;kafka是一个分布式&#xff0c;分区的&#xff0c;多副本的&#xff0c;多订阅者的消息队列系统。 2、Kafka相比其他消息队列的优势 ​ 常见的消息队列&#xff1a;Rabbit…

【设计模式】张一鸣笔记:责任链接模式怎么用?

我将通过一个贴近现实的故事——请假审批流程&#xff0c;带你了解和掌握责任链模式。 什么是责任链模式&#xff1f; 责任链模式是一种行为设计模式&#xff0c;它让你可以避免将请求的发送者与接收者耦合在一起&#xff0c;让多个对象都有处理请求的机会将这个对象连成一条…

MQ 消息丢失、重复、积压问题,如何解决?

面试官在面试候选人时&#xff0c;如果发现候选人的简历中写了在项目中使用了 MQ 技术&#xff08;如 Kafka、RabbitMQ、RocketMQ&#xff09;&#xff0c;基本都会抛出一个问题&#xff1a;在使用 MQ 的时候&#xff0c;怎么确保消息 100% 不丢失&#xff1f; 这个问题在实际…

2024-01-22(MongoDB)

1.Mongodb使用的业务场景&#xff1a; 传统的关系型数据库/mysql在“三高”需求以及应对web2.0的网站需求面前&#xff0c;有点力不从心&#xff0c;什么是“三高”需求&#xff1a; a. 对数据库高并发的读写需求 b. 对海量数据的高效率存储和访问需求 c. 对数据库的高可扩…

wayland(wl_shell) + egl + opengles 实例——gears

文章目录 前言一、ubuntu 上 opengl 版本的 glxgears二、基于 wayland 窗口协议的 gles-gears1.egl_wayland_gears.c2. matrix.c 和 matrix.h3. 编译4. 运行总结参考资料前言 本文主要介绍如何在linux 下 wayland 窗口中,使用 egl + opengles 控制GPU 渲染 gears 并显示,即实…

element plus表格的表头和内容居中

文章目录 需求分析 需求 对于 element-plus 中的 table 进行表头和内容的居中显示 分析 单列的表头和内容居中 &#xff1a; 在对应的那一列加上align“center” 即可 <el-table-column prop"name" label"商品名称" align"center" />…

统计灰度图像的灰度值分布并绘制

1、numpy方法 函数&#xff1a; numpy.histogram(a, bins10, rangeNone, normedNone, weightsNone, densityNone) 参数说明&#xff1a; a:输入数据数组&#xff1b;bins:指定统计的区间个数&#xff0c;可以是一个整数&#xff0c;也可以是一个数组&#xff0c;默认值为10…

华而有实,维乐Prevail Glide带你领略风景线,成为风景线~

大家都知道呢&#xff01;骑行&#xff0c;不仅是一种运动&#xff0c;更是一种生活态度。在骑行装备的世界里&#xff0c;一个好的坐垫对于骑行的舒适度和安全性至关重要。那今天&#xff0c;我要为大家推荐一款备受赞誉的坐垫——维乐坐垫美学系列-Prevail Glide。    为…

【C++】初识类和对象

引言 在C语言中&#xff0c;我们用结构体来描述一个复杂的对象&#xff0c;这个对象可能包括许多的成员&#xff0c;如用结构体描述一个学生的成绩&#xff0c;或者描述一个日期等。 struct Date {int _year;int _month;int _day; }; 如上是一个描述日期的结构体定义&#x…

一本满是错误的Go语言书,凭什么1000万人都在读

犯错是每个人生活的一部分。正如爱因斯坦曾说过&#xff1a;一个从未犯过错的人从未尝试过新东西。 最重要的不是我们犯了多少错误&#xff0c;而是我们从错误中学到了多少东西。 这个观点同样适用于编程领域。 我们从一门编程语言中获取经验不是一个神奇的过程&#xff0c;…

Rocky Linux 9. 3安装图解

风险告知 本人及本篇博文不为任何人及任何行为的任何风险承担责任&#xff0c;图解仅供参考&#xff0c;请悉知&#xff01;本次安装图解是在一个全新的演示环境下进行的&#xff0c;演示环境中没有任何有价值的数据&#xff0c;但这并不代表摆在你面前的环境也是如此。生产环境…

2024.1.19 网络编程 作业

思维导图 练习题 1> UDP传输实现聊天室 服务器端 #include <myhead.h> #define SER_IP "192.168.125.151" #define SER_PORT 9999 typedef struct Msg {char user[32]; //用户名int type; //执行操作1.登录、2.发消息、0.退出char text[1024]; …

NOC总线(1)

1. 背景 SoC &#xff08;system on chip,片上系统&#xff09;通常指在单一芯片上实现的数字计算机系统&#xff0c;总线结构是该系统的主要特征&#xff0c;由于其可以提供高性能的互连而被广泛运用。随着单芯片上集成的处理器核数越来越多&#xff0c;片上互连架构经历了从专…

sqlmap使用教程(2)-连接目标

目录 连接目标 1.1 设置认证信息 1.2 配置代理 1.3 Tor匿名网络 1.4 检测WAF/IPS 1.5 调整连接选项 1.6 处理连接错误 连接目标 场景1&#xff1a;通过代理网络上网&#xff0c;需要进行相应配置才可以成功访问目标主机 场景2&#xff1a;目标网站需要进行身份认证后才…

【git分支管理策略】

文章目录 前言一、分支管理策略简介二、git基本操作三、git分支远程分支本地分支 四、gitflow分支管理策略分支定义gitflow分支管理策略评价 五、GITHUB FLOW分支管理策略分支使用流程创建分支&#xff08;Create a branch&#xff09;新增提交(add and commit)提出 Pull 请求&…

关联系统-智能座舱控制器ICC

智能座舱构成 如上图所示&#xff0c;智能座舱主要是由仪表、中控、HUD、语音、DMS/OMS等多种交互通道组成&#xff0c;其宗旨是提升人的交互体验&#xff0c;使车辆更加智能化&#xff0c;情感化。 智能座舱内部功能 仪表功能 SR场景重构 如上图所示&#xff0c;仪表区域可实…

k8s的包管理工具helm

Helm是什么? 之前的这篇文章介绍了一开始接触k8s的时候接触到的几个命令工具 kubectl&kubelet&rancher&helm&kubeadm这几个命令行工具是什么关系&#xff1f;-CSDN博客 Helm 是一个用于管理和部署 Kubernetes 应用程序的包管理工具。它允许用户定义、安装和…

Linux软件包管理器yum

文章目录 前言概述Linux下载软件的三种方式源代码安装rpm安装yum安装 关于yum的相关操作查看软件包软件安装卸载软件 yum源问题 前言 在Windows系统中&#xff0c;如果我们要去下载软件&#xff0c;我们可以在该软件的官网中进行下载&#xff0c;或者在微软的额软件商店进行下…

设计亚马逊按销售排名功能

1&#xff1a; 定义 Use Cases 和 约束 Use cases 作用域内的Use Case Service 通过目录计算过去一周内最受欢迎的产品User 通过目录去View过去周内最受欢迎的产品Service 有高可用 作用域外 整个电商网站 设计组件&#xff08;只是计算销售排名&#xff09; 约束和假设…

经典面试题-死锁

目录 1.什么是死锁&#xff1f; 2.形成死锁的四个必要条件 3.死锁的三种情况 第一种情况&#xff1a; 举例&#xff1a; 举例&#xff1a; 第二种情况&#xff1a;两个线程 两把锁 举例&#xff1a; 第三种情况&#xff1a;N个线程 M把锁 哲学家进餐问题 1.什么是死锁&…