左右值的概念
https://corecppil.github.io/CoreCpp2019/Presentations/Dan_Saks_Lvalues_and_Rvalues.pdf
简述
原版PPT 有31页,我主要摘取几个重要的点。
下面所说的对象都是广义的对象(object) 一个int float 都可以看作一个对象。而 类对象 与区分。
最后的总结是我自己总结的。
正文翻译
概览
- 左右值并非C++ 语言特性,相反,它是表达式的属性。
- 本篇将会通过以下视角理解左右值:
- 内置运算符的行为
- 为执行操作而生成的汇编
- 相关编译器错误含义
- 引用类型
- 重载运算符
- 左右值的含义已经发生变化
- 在早期C ,左右值得概念非常简单。
- 早期C++,增加了类,const,引用,使得左右值概念复杂起来。
- 现代C++增加了右值引用,使得左右值进一步复杂
- 本文从历史起源来解释左右值
左值
在 The C Programming Language 中,有以下的定义:
-
左值 起源于 赋值表达式
E1 = E2
,其中左操作数E1
必须为左值表达式。 -
一个左值是一个和对象相关的表达式。而一个对象是内存的一块区域。
int n; //定义一个int 对象命名为 n n = 1; //赋值表达式
在赋值表达式中:
- n 是一个子表达式,和int 对象相关。它是左值
- 1 是一个子表达式,但是没有和某个对象相关。它是右值
-
所以右值的定义就是非左值
深入底层
为什么需要区分左值和右值?
- 编译器可以假定右值不需要存储空间。(这块意思右值逻辑上不占空间,所以没有引用数组),
- 这在为右值表达式生成代码提供了足够的自由。
继续上面的例子 n = 1
:
-
如果 1 是左值的话,编译器认为 1 和一个初始化为1 的内存中对象相关联,假定这块区域称为 one
-
当执行赋值编译器会生成类似于 mov n, one 即将one 所代表区域的数据拷贝到 n 所代表区域。
-
而在某些CPU 上,提供直接操作数的功能,类似于 mov n, 1 即将值1 拷贝到n 所代表的区域。
-
在这种情况下,右值永远不会作为一个存储在数据空间中的对象。相对的,它作为代码空间指令的一部分。
-
在某些CPU 上,当将 1 赋值给对象,可能会采取这个方式: clr n inc n 即先将n 清零,再增加1 。
-
假设有这个例子 1 = n
:
- 显然C/C++都会报错。但是精确原因是什么?
- 赋值语句将一个值赋值给一个对象。
- 左操作数必须为左值
- 而1 是右值。
总结:所有C 中表达式 要么是左值,要么是右值。左值和数据空间中对象相关联,右值是非左值。对于非类类型的C++ 也是正确的。但如果有类类型,就不那么准确(类对象通常不管左右值都保存在数据空间)。
其它类型
字面值:大部分字面量都是右值(1,3.2,‘c’ 等),它们不一定占用数据空间。但有些字面量(“abcdefg” 等) 却是左值,占用数据空间。
枚举常量:枚举常量也是右值。
将左值用作右值
如果是非类类型,就直接将左值视为右值。如果是类类型,将会执行左值到右值的转换。
表达式的结果
表达式例如 m + n
产生的结果存放在编译器生成的临时对象,通常存放在寄存器中。像这些的临时对象都是右值。
所以 m + 1 = n
先计算 m + 1 产生右值,向右值赋值产生错误。
再例如 &n
&1
其运算对象必须是左值,因为右值和数据空间中对象无关,不可寻址。它产生的结果却是右值。
再次强调 左值是编译期的属性,如果 *p
它运算对象可以是左值,也可以是右值例如( *(p+1) ),但它返回的是左值。即使p 指向nullptr ,编译也不会出错,不过运行会出错。
在C/C++ 中,非类类型的右值不会占用数据空间。而任何类型的左值都会占用。(虽然有时候编译器会优化左值,使其不占用空间,但你应该假定左值都是占用空间)。
常对象
常对象是可寻址的对象。
编译器可能会存储常对象,也可能会优化掉(例如C 中常量是常变量,而 C++ 中会被编译器替换)
C++引用类型
掌握以上左值和右值概念有助于理解C++的引用,引用使C++ 重载运算符的行为类似于内置运算符。
引用用于关联有名对象。引用底层是常量指针,自动解引用产生左值。
enum month {
Jan, Feb, Mar, ~~~, Dec, month_end
};
typedef enum month month;
~~~
for (month m = Jan; m <= Dec; ++m) {
~~~
}
这样的代码在C 中可以正常工作,然而在C++中编译错误。内置的++ 不能用于枚举变量。
常量引用
常量引用可以绑定到非常量,也可以绑定到常量。而普通引用只能绑定非常量。
常量引用自动解引用产生不可改的左值。
类似于普通 指针,普通引用只能绑定到左值上。
常量引用既可以绑定到左值也可以绑定到右值。
const int &thr = 3;
在这种情况下,运行到这时,程序会创建一个int 临时量,其值为3。然后用thr 绑定这个临时量。当离开 thr 的范围,程序销毁这个临时量。
const double &dthr = thr;
程序先会将thr 的值转化为 double ,创建double 临时量,然后保存转化的值。之后用 dthr 绑定到这个临时量。最后,离开范围会销毁这个临时量。
右值引用
在C++ 03 中称为引用的,在C++ 11 中称为 左值引用,为了与新增的右值引用所区分。
右值引用只能绑定右值,哪怕是常右值引用。而常左值引用也可以绑定右值。
现代C++ 通常使用右值操作来避免不必要的拷贝。
与成员函数
成员函数可以用引用限定符重载,左右值调用不同版本。
总结
左右值这个概念最初是从 C 语言来的,其为了CPU 能做某些优化而设置。此时的概念是和内存中区域相关的对象都成为左值,而右值是它的补集。右值不占内存空间。右值通常都是存在 CPU 中的数据。而字面值有可能右值(‘c’),也有可能是左值(“asdfdsasd”)。
到了早期C++ (C++ 11 之前),出现了引用等。此时右值有时也占用空间(例如右值类对象)。
到了现代C++ (C++ 11及以后),出现了左值引用,右值引用的概念。为了与右值引用区分,之前的引用统称为左值引用。右值引用的出现主要是为了避免不必要的拷贝。