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及以后),出現了左值引用,右值引用的概念。為了與右值引用區分,之前的引用統稱為左值引用。右值引用的出現主要是為了避免不必要的拷貝。
