C++中有「左值」、「右值」的概念,C++11之後,又有了「左值」、「純右值」、「將亡值」的概念。關於這些概念,許多資料上都有介紹,本文在拾人牙慧的基礎上又加入了一些本身的一些理解,同時提出了一些須要讀者特別注意的地方,主要目的有二:
1.儘量地將這些概念介紹清楚。
2.爲後續介紹完美轉發和移動語義作好鋪墊。html
正文c++
1、表達式程序員
要說清「三值」,首先要說清表達式。數組
定義
由運算符(operator)和運算對象(operand)①構成的計算式(相似於數學上的算術表達式)。
舉例
字面值(literal)和變量(variable)是最簡單的表達式,函數的返回值也被認爲是表達式。函數
2、值類別spa
表達式是可求值的,對錶達式求值將獲得一個結果(result)。這個結果有兩個屬性:類型和值類別(value categories)。下面咱們將詳細討論表達式的值類別②。指針
在c++11之後,表達式按值類別分,必然屬於如下三者之一:左值(left value,lvalue),將亡值(expiring value,xvalue),純右值(pure rvalue,pralue)。其中,左值和將亡值合稱泛左值(generalized lvalue,glvalue),純右值和將亡值合稱右值(right value,rvalue)。見下圖c++11
有一點須要說明,嚴格來說,「左值」是表達式的結果的一種屬性,但更爲廣泛地,咱們一般用「左值」來指代左值表達式(正如上邊一段中作的那樣)。所謂左值表達式,就是指求值結果的值類別爲左值的表達式。一般咱們無需區分「左值」指的是前者仍是後者,由於它們表達的是同一個意思,不會引發歧義。在後文中,咱們依然用左值指代左值表達式。對於純右值和將亡值,亦然。code
3、詳細說明htm
事實上,不管是左值、將亡值仍是純右值,咱們目前都沒有一個精準的定義。它們事實上表徵了表達式的屬性,而這種屬性的區別主要體如今使用上,如可否作運算符的左操做數、可否使用移動語義(關於移動語義,在下的後續文章中會詳細介紹)等。所以,從實際應用出發,咱們首先須要作到的是:給定一個表達式,可以正確地判斷出它的值類別。爲了使讀者可以作到這一點,在下采起了一個實際的方式:先對各個值類別的特徵加以描述,而後指出常見的表達式裏邊,哪些屬於該類別。
左值
描述
可以用&取地址的表達式是左值表達式。
舉例
函數名和變量名(其實是函數指針③和具名變量,具名變量如std::cin、std::endl等)、返回左值引用的函數調用、前置自增/自減運算符鏈接的表達式++i/--i、由賦值運算符或複合賦值運算符鏈接的表達式(a=b、a+=b、a%=b)、解引用表達式*p、字符串字面值"abc"(關於這一點,後面會詳細說明)等。
純右值
描述
知足下列條件之一:
1)自己就是赤裸裸的、純粹的字面值,如三、false;
2)求值結果至關於字面值或是一個不具名的臨時對象。
舉例
除字符串字面值之外的字面值、返回非引用類型的函數調用、後置自增/自減運算符鏈接的表達式i++/i--、算術表達式(a+b、a&b、a<<b)、邏輯表達式(a&&b、a||b、~a)、比較表達式(a==b、a>=b、a<b)、取地址表達式(&a)等。
下面從上面的例子中選取若干典型詳細說明左值和純右值的判斷。
1)++i是左值,i++是右值。
前者,對i加1後再賦給i,最終的返回值就是i,因此,++i的結果是具名的,名字就是i;而對於i++而言,是先對i進行一次拷貝,將獲得的副本做爲返回結果,而後再對i加1,因爲i++的結果是對i加1前i的一份拷貝,因此它是不具名的。假設自增前i的值是6,那麼,++i獲得的結果是7,這個7有個名字,就是i;而i++獲得的結果是6,這個6是i加1前的一個副本,它沒有名字,i不是它的名字,i的值此時也是7。可見,++i和i++都達到了使i加1的目的,但兩個表達式的結果不一樣。
2)解引用表達式*p是左值,取地址表達式&a是純右值。
&(*p)必定是正確的,由於*p獲得的是p指向的實體,&(*p)獲得的就是這一實體的地址,正是p的值。因爲&(*p)的正確,因此*p是左值。而對&a而言,獲得的是a的地址,至關於unsigned int型的字面值,因此是純右值。
3)a+b、a&&b、a==b都是純右值
a+b獲得的是不具名的臨時對象,而a&&b和a==b的結果非true即false,至關於字面值。
將亡值
描述
在C++11以前的右值和C++11中的純右值是等價的。C++11中的將亡值是隨着右值引用④的引入而新引入的。換言之,「將亡值」概念的產生,是由右值引用的產生而引發的,將亡值與右值引用息息相關。所謂的將亡值表達式,就是下列表達式:
1)返回右值引用的函數的調用表達式
2)轉換爲右值引用的轉換函數的調用表達式
讀者會問:這與「將亡」有什麼關係?
在C++11中,咱們用左值去初始化一個對象或爲一個已有對象賦值時,會調用拷貝構造函數或拷貝賦值運算符來拷貝資源(所謂資源,就是指new出來的東西),而當咱們用一個右值(包括純右值和將亡值)來初始化或賦值時,會調用移動構造函數或移動賦值運算符⑤來移動資源,從而避免拷貝,提升效率(關於這些知識,在後續文章講移動語義時,會詳細介紹)。當該右值完成初始化或賦值的任務時,它的資源已經移動給了被初始化者或被賦值者,同時該右值也將會立刻被銷燬(析構)。也就是說,當一個右值準備完成初始化或賦值任務時,它已經「將亡」了。而上面1)和2)兩種表達式的結果都是不具名的右值引用,它們屬於右值(關於「不具名的右值引用是右值」這一點,後面還會詳細解釋)。又由於
1)這種右值是與C++11新生事物——「右值引用」相關的「新右值」
2)這種右值經常使用來完成移動構造或移動賦值的特殊任務,扮演着「將亡」的角色
因此C++11給這類右值起了一個新的名字——將亡值。
舉例
std::move()、tsatic_cast<X&&>(x)(X是自定義的類,x是類對象,這兩個函數經常使用來將左值強制轉換成右值,從而使拷貝變成移動,提升效率,關於這些,後續文章中會詳細介紹。)
附註
事實上,將亡值不過是C++11提出的一塊晦澀的語法糖。它與純右值在功能上及其類似,如都不能作操做符的左操做數,均可以使用移動構造函數和移動賦值運算符。當一個純右值來完成移動構造或移動賦值任務⑥時,其實它也具備「將亡」的特色。通常咱們沒必要刻意區分一個右值究竟是純右值仍是將亡值。
關於「三值」的大致介紹,就到此結束了。想要獲知更加詳細的內容,讀者能夠參考cppreference上的文章:
http://naipc.uchicago.edu/2015/ref/cppreference/en/cpp/language/value_category.html (精簡版)
和
http://en.cppreference.com/w/cpp/language/value_category (詳細版)
文章對「三值」進行了詳細地講述,同時講出了將左值和將亡值合稱泛左值的緣由(這是本文未詳細討論的),如二者均可以使用多態,均可以隱式轉換成純右值,均可以是不徹底類型(incomplete type)等。之因此不展開敘述,是由於在下實在舉不出合適的代碼來加以佐證。這裏在下懇請各位讀者不吝賜教。另外,關於文章(特別是詳細版)中的一些觀點,在下不敢苟同,篇幅緣由,在下就不一一敘述了。
4、特別注意
最後,關於「三值」,有些地方須要你們特別注意。
1)字符串字面值是左值。
不是全部的字面值都是純右值,字符串字面值是惟一例外。
早期C++將字符串字面值實現爲char型數組,實實在在地爲每一個字符都分配了空間而且容許程序員對其進行操做,因此相似
cout<<&("abc")<<endl; char *p_char="abc";//注意不是char *p_char=&("abc");
這樣的代碼都是能夠編譯經過的。
注意上面代碼中的註釋,"abc"能夠直接初始化指針p_char,p_char的值爲字符串"abc"的首字符a的地址。而&("abc")被編譯器編譯爲const的指向數組的指針const char (*) [4](之因此是4,是由於編譯器會在"abc"後自動加上一個'\0'),它不能初始化char *類型,即便是const char *也不行。另外,對於char *p_char="abc";,在GCC編譯器上,GCC4.9(C++14)及之前的版本會給出警告,在GCC5.3(C++14)及之後的版本則直接報錯:ISO C++ forbids converting a string constant to 'char*'(ISO C++禁止string常量向char*轉換)。但這並不影響「字符串字面值是左值」這一結論的正確性,由於cout<<&("abc")<<endl;一句在各個版本的編譯器上都能編譯經過,沒有警告和錯誤。
2)具名的右值引用是左值,不具名的右值引用是右值。
見下例(例一)
void foo(X&& x) { X anotherX = x; //後面還能夠訪問x }
上面X是自定義類,而且,其有一個指針成員p指向了在堆中分配的內存;參數x是X的右值引用。若是將x視爲右值,那麼,X anotherX=x;一句將調用X類的移動構造函數,而咱們知道,這個移動構造函數的主要工做就是將x的p指針的值賦給anotherX的p指針,而後將x的p指針置爲nullptr。(後續文章講移動構造函數時會詳細說明)。而在後面,咱們還能夠訪問x,也就是能夠訪問x.p,而此時x.p已經變成了nullptr,這就可能發生意想不到的錯誤。
又以下例(例二)
X& foo(X&& x) { //對x進行一些操做 return x; } //調用 foo(get_a_X());//get_a_X()是返回類X的右值引用的函數
上例中,foo的調用以右值(確切說是將亡值)get_a_X()爲實參,調用類X的移動構造函數構造出形參x,而後在函數體內對x進行一些操做,最後return X,這樣的代碼很常見,也很符合咱們的編寫思路。注意foo函數的返回類型定義爲X的引用,若是x爲右值,那麼,一個右值是不能綁定到左值引用上去的。
爲避免這種狀況的出現,C++規定:具名的右值引用是左值。這樣一來,例一中X anotherX = x;一句將調用X的拷貝構造函數,執行後x不發生變化,繼續訪問x不會出問題;例二中,return x也將獲得容許。
例二中,get_a_X返回一個不具名右值引用,這個不具名右值引用的惟一做用就是初始化形參x,在後面的代碼中,咱們不會也沒法訪問這個不具名的右值引用。C++將其歸爲右值,是合理的,一方面,可使用移動構造函數,提升效率;另外一方面,這樣作不會出問題。
至此,關於「三值」的內容就所有介紹完了。
註釋:
①只有當存在兩個或兩個以上的運算對象時才須要運算符鏈接,單獨的運算對象也能夠是表達式,例如上面提到的字面值和變量。
②確切說,是表達式的結果的值類別,但咱們通常不刻意區分表達式和表達式的求值結果,因此這裏稱「表達式的值類別」。
③當咱們將函數名做爲一個值來使用時,該函數名自動轉換爲指向對應函數的指針。
④關於右值引用自己,沒什麼可說的,就是指能夠綁定到右值上的引用,用"&&"表示,如int &&rra=6;。相比之下,與右值引用相關的一些主題,如移動語義、引用疊加、完美轉發等,更值得咱們深刻探討。這些內容,在下在後續文章中都會詳細介紹。
⑤前提是該右值(如自定義的類X)有移動構造函數或移動賦值運算符可供調用(有時候是沒有的,關於這些知識,後續文章在講移動構造函數和移動賦值運算符時會詳述)。
⑥在本文的例二中,若是將get_a_X()的返回值由X的右值引用改成X對象,則get_a_X()是純右值表達式(如前所述,返回非引用類型的函數調用是純右值),此時Foo(get_a_X());一句調用的仍然是類X的移動構造函數,這就是一個純右值完成移動構造的例子。
寫在後面
在下在參閱許多資料以後,再結合本身的理解,整理出了這篇文章,力圖能實如今下寫博客(不光是這篇,是全部)的初衷——爲初學者服務,儘可能把話說明白。可是,因爲「三值」問題自己較爲複雜,再加上在下才疏學淺,表達能力有限,錯誤疏漏及其它不足之處在所不免。因此,但願廣大讀者可以用批判的眼光來閱讀這篇文章,更懇請你們對在下的錯誤疏漏提出批評指正。您的批評指正,既是對在下莫大的幫助,更是在下進步的力量源泉。