Loading

左右值的概念

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及以后),出现了左值引用,右值引用的概念。为了与右值引用区分,之前的引用统称为左值引用。右值引用的出现主要是为了避免不必要的拷贝。

posted @ 2020-06-18 14:58  沉云  阅读(1450)  评论(0编辑  收藏  举报