当前位置: 首页 > article >正文

C++ 左值与右值浅谈

左值与右值

  • 序言
  • 概念
    • 左值和右值的划分理解
    • 右值引用
      • 常量左值引用与右值引用
    • 移动语义
    • 引用折叠
    • 完美转发
  • 参考资料

序言

虽然平常都算是了解左值,右值的用法,但是好记性不如烂笔头,记下来供大家评鉴,有错改错,有善赞善,也是对于自己知识的一次梳理。

为什么要分清楚左值和右值?这是因为在理清楚左值和右值,合适为其设置适合的用法,能够有效减少资源开销。

但是,对于一些POD类型的资源,那就无所谓左值右值了,因为拷贝即移动,移动即拷贝。

接下来我以左值和右值的讨论依次简单讲解:左值和右值的概念,右值引用,移动语义,引用折叠,完美转发。明确左值和右值理清楚后,可以使用的主要用法。

:1. 开始看下面之前,需要注意的是!这些除了专业名词之外,基本都是基于个人理解去通俗诠释概括的,想直接看专业且全面的概念就找末尾的参考资料看。
2.以下有比较多的专业词汇,初学者建议慢慢看查询拓展。

概念

左值和右值的划分理解

左值(lvalue)和右值(rvalue)是C++11之前的概念,但是也通用到后面。
C++11及之后,划分为 泛左值(glvalue)、将亡值(xvalue,也称亡值,消亡值)和纯右值(prvalue)

左值(C++11之前):赋值运算“=”左边的变量
右值(C++11之前):赋值运算“=”右边的表达式

左值(C++11及之后):非将亡值的泛左值,有地址的变量
右值(C++11及之后):纯右值或者将亡值,生命周期在表达式里。
int a = 15 + 29;
std::cout << &a;	// 0xeffc40
std::cout << &(15 + 29);	//error: Cannot take the address of an rvalue of type 'int'
std::cout << &"xzz";	//0xa16444

以简单的例子,这个a,承载类型的值,自身是有地址的,可以取地址值,这个就是左值。
15 + 29这个表达式的结果是纯右值,不能取地址值。

注:顺带一提,许多普通常量都是纯右值,但是字符串不是,是左值,因为普通常量都是可以用普通的机器码就可以表示其值,但是字符串无法合适表示,所以将其放置在常量区分配内存专门存放。

想必想了解左值和右值的人,估计都看过这个图:
在这里插入图片描述
或者是类似的,基本都是说将亡值泛左值右值的交集。

但是这其实是容易让人摸不着头脑的,但是本质角度上又是能说得过去的。

1. 将亡值被包含在右值这边,是因为其的    生命周期和右值是一样     的,都在一个表达式里面。
2. 将亡值被包含在泛左值这边,是因为其是      匿名对象,有地址,和左值是一样     的。

而上述也引申出了怎么判断将亡值。

将亡值:生命周期在一个表达式里,且是匿名对象有地址。

C++17的临时量实质化也是将亡值。

.

右值引用

右值引用T &&),顾名思义是引用右值的,无论是纯右值还是将亡值

右值引用是C++11引入的,值得注意的是右值引用的变量是个左值

因为其是完全符合左值定义的,众所周知,引用本质上是一种特殊的指针,可以这么认为,指针指向的值是右值,但是指针本身并不是右值。

所以你如果想右值引用 右值引用的变量 这样是不行的:

int &&a = 5;	// 编译正常,可以随意右值引用纯右值
int &&b = a;   	// 编译错误

右值引用的目的是延长将亡值的生命周期,减少资源开销,或者是为了移动语义服务,使其进行资源转移。

struct AA {};
AA createAA() {
	return AA();
}
int main() {
	AA &&a = createAA(); 
}

右值引用AA &&a接纳了本来表达式结束就要释放掉资源的匿名对象AA()

并可以任意更改匿名对象的资源。

常量左值引用与右值引用

在C++11之前,负责右值引用(T &&)功能的是常量左值引用(const T &),只不过和右值引用相比,常量左值引用无法修改其值,且只能用于拷贝语义不能用以移动语义
在这里插入图片描述
在这里插入图片描述
可以看出来,常量左值引用右值引用做的事情是一样的。

顺带一提,不建议用右值引用去引用POD类型的纯右值,因为纯右值要想被右值引用,就得先压栈地址,才能给其引用。

从开销上看,不如直接普通的赋值
在这里插入图片描述
在这里插入图片描述
就算单纯只看条数,右值引用用了3条,普通赋值才用了1条,开销一目了然。

.

移动语义

移动语义(Move Semantics)是 C++11 引入的一项重要特性,它使得实例对象的资源不通过拷贝的方式进行转移(除了POD类型)。

移动语义具体化其实就是移动构造函数。

struct Resource { ... }

class XZZ 
{
public:
	...
	构造或者其他的实例化资源
	...
	移动构造函数
	XZZ(XZZ &&value) {
		this.m_resource = value.m_resource;
		value.m_resource = nullptr;
	}
	/// 移动赋值函数
	XZZ &operator =(XZZ &&value) {
		this.m_resource = value.m_resource;
		value.m_resource = nullptr;
	}
	...
private:
	Resource *m_resource = nullptr;
}

上面是个简单的例子,主要是为了理解移动是怎么来移动资源的。

如果m_resource不是指针,也可以通过使用std::move强行将value.m_resource转成右值来触发this.m_resource的移动构造,使得两个m_resource的资源进行移动达到同样的效果。

_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept {
    return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

std::move的效果便是强制将传进来的参数转成右值,一般可以将已经右值引用的变量或者将要释放的类型转成右值(将亡值),实现移动语义的功能。

再次提醒,如果资源是POD类型的,那用移动语义其实没有意义,因为移动就是拷贝,拷贝就是移动。

另外移动构造什么情况下可以编辑器会提供默认移动构造,什么情况下会弃置默认移动构造只能自己写的,这些内容不在本节重点,感兴趣可自行查看。
.

引用折叠

说回右值引用的类型,左值引用右值引用,或者右值引用左值引用,那到底是左值引用还是右值引用呢?

C++11中引入引用折叠规则(reference collapsing),通过模板或 typedef 中的类型操作可以构成引用的引用,此时适用引用折叠规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用

废话不多说:
C++ 11新特性解析与应用
简单通俗来说,只有右值引用本身,和叠加两次的右值引用,类型才是右值引用类型,否则,含至少一个引用&的都是左值引用

不能直接声明一个超过两个&的类型

int a = 10;
int &&& b = a;	// error: 'b' declared as a reference to a reference

但是如果通过using或者typedef间接声明就可以了

typedef int& intR;
using intRe = int&;

int a = 10;
intR && b = a;		// 等同于 int &b = a
intRe && c = a;		// 等同于 int &c = a

有了引用折叠,就可以好好使用类型擦除完美转发参数类型给别的函数或者类。
.

完美转发

所谓完美转发(prefect forwarding),是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。 ——《C++ 11新特性解析与应用》

完美转发关键点在于:

  1. 函数模板
  2. 函数模板参数类型是 类型&&
  3. 要接收函数模板参数的函数/类,实参用std::forward包装一下
template <typename _Ty, typename... _Type>
_Ty *createClass(_Type&&... args) {
    return new _Ty(std::forward(args)...);
}

这是个没什么实质意义的模板函数,仅是为了举例。

为什么要用std::forward

是因为右值引用args本身是左值,传进来本身如果是个右值的话,结果给到接收函数是个左值,那就不是“完美”转发了。

_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept {
    return static_cast<_Ty&&>(_Arg);
}

_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept {
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
    return static_cast<_Ty&&>(_Arg);
}

所以需要转成右值的类型,而如果是左值的话,因为函数重载引用折叠的缘故,即使通过std::forward也是转成左值类型。

参考资料

《C++ 11新特性解析与应用》
《C/C++ 参考文档》


http://www.kler.cn/a/302749.html

相关文章:

  • 【Linux】-学习笔记03
  • linux c/c++最高效的计时方法
  • 卓胜微嵌入式面试题及参考答案(2万字长文)
  • MySQL数据库:SQL语言入门 【下】(学习笔记)
  • AI 写作(五)核心技术之文本摘要:分类与应用(5/10)
  • L10.【LeetCode笔记】回文链表
  • 每天一道面试题(9):lock 和 synchronized 区别
  • C# WPF中实现图表生成的五种方式
  • 【SpringCloud】微服务架构演进与Spring Cloud简介
  • 基于spring的博客系统(二)
  • Go Playground 在线编程环境
  • 优购电商小程序的设计与实现+ssm(lw+演示+源码+运行)
  • MySql8.x---开窗函数
  • HTTP 协议介绍
  • JS手写实现深拷贝
  • mysql性能优化-云服务与数据库即服务(DBaaS)优化
  • [论文笔记] LLM大模型剪枝篇——2、剪枝总体方案
  • 【JAVA入门】Day34 - Stream流
  • facebook广告和谷歌广告如何选择效果最大化?
  • eclipse配置maven
  • 第T11周:优化器对比实验
  • 【TCP】相关机制:异常处理
  • 泰州高新区法院多层面强化固定资产管理
  • 【数据结构篇】~排序(1)之插入排序
  • 众店绿色积分模式:引领消费新风尚,共筑商业新生态
  • 数据结构算法和算法分析