C++11 左值、右值與右值引用


【1】左值與右值

在C語言中,常常會提起左值(lvalue)、右值(rvalue)這樣的稱呼。

而在編譯程序時,編譯器有時也會在報出的錯誤信息中包含左值、右值的說法。

不過左值、右值通常不是通過一個嚴謹的定義而為人所知的。

事實上,之所以只知道一些關於左值、右值的判斷而很少聽到其真正的定義的一個原因就是很難歸納。而且即使歸納了,仍需要大量的解釋。

(1)左右值的判別方法一

大多數時候,左右值的一個最為典型的判別方法:

在賦值表達式中,出現在等號左邊的就是“左值”,而在等號右邊的,則稱為“右值”。比如:

a = b + c;

在這個賦值表達式中,a就是一個左值,而b+c則是一個右值。

這種識別左值、右值的方法在C++中依然有效。

(2)左右值判別方法二

不過C++中還有一個被廣泛認同的說法,那就是:

可以取地址的、有名字的就是左值,反之,不能取地址的、沒有名字的就是右值。

那么這個加法賦值表達式中,&a是允許的操作,但&(b + c)這樣的操作則不會通過編譯。

因此a是一個左值,(b+c)是一個右值。這些判別方法通常都非常有效。

(3)左右值判別方法三

更為細致地,在C++11中,右值是由兩個概念構成的,一個是純右值(prvalue,Pure Rvalue),另一個則是將亡值(xvalue,eXpiring Value)。

其中純右值就是C++98標准中右值的概念,指的是用於辨識臨時變量和一些不跟對象關聯的值。

比如:返回類型為非引用的函數返回的臨時變量值就是一個純右值。

一些運算表達式,比如1+3產生的臨時變量值,也是純右值。

而不跟對象關聯的字面量值,比如:2、‘c’、true,也是純右值。

此外,類型轉換函數的返回值、lambda表達式等,也都是右值。

將亡值則是C++11新增的跟右值引用相關的表達式,這樣表達式通常是將要被移動的對象(移為他用),比如:

[1] 返回右值引用T&&的函數返回值

[2] std::move的返回值

[3] 轉換為T&&的類型轉換函數的返回值。

而剩余的,可以標識函數、對象的值都屬於左值

所以,在C++11的程序中,所有的值必屬於左值、將亡值、純右值三者之一。

【2】左值引用與右值引用

在C++11中,右值引用(rvalue reference)就是對一個右值進行引用的類型。

為了區別於之前C++98中的引用類型,我們稱C++98中的引用為“左值引用”(lvalue reference)。

右值引用和左值引用都是屬於引用類型,無論是聲明一個左值引用還是右值引用,都必須立即進行初始化。

其原因可以理解為是引用類型本身自己並不擁有所綁定對象的內存,只是該對象的一個別名。

左值引用是具名變量值的別名,而右值引用則是不具名(匿名)變量的別名。

事實上,由於右值通常不具有名字,我們也只能通過引用的方式找到它的存在。

通常情況下,我們只能是從右值表達式獲得其引用。比如:

T && a = ReturnRvalue();

這個表達式中,假設ReturnRvalue返回一個右值,我們就聲明了一個名為a的右值引用,其值等於ReturnRvalue函數返回的臨時變量的值。

ReturnRvalue函數返回的右值在表達式語句結束后,其生命也就終結了(通常我們也稱其具有表達式生命期)。

而通過右值引用的聲明,該右值又“重獲新生”,其生命期將與右值引用類型變量a的生命期一樣。

即就是,只要a還“活着”,該右值臨時量將會一直“存活”下去。所以,相比於以下語句的聲明方式:

T b = ReturnRvalue();

剛才的右值引用變量聲明,就會少一次對象的析構及一次對象的構造。

因為a是右值引用,直接綁定了ReturnRvalue()返回的臨時量,而b只是由臨時值構造而成的,而臨時量在表達式結束后會被析構掉,因應就會多一次析構和構造的開銷。

關於右值引用,可以參考下例加深理解:

 1 #include <iostream>
 2 
 3 void f(int& i) { std::cout << "call left value ref :: " << i << "\n"; }
 4 void f(int&& i) { std::cout << "call right value ref :: " << i << "\n"; }
 5 
 6 int main()
 7 {
 8     int i = 2020;
 9     f(i);      // call left value ref :: 2020
10     f(1010);   // call right value ref :: 1010
11     f(std::move(i));  // call right value ref :: 2020
12 
13     system("pause");
14     return 0;
15 }
16 
17 /*
18 call left value ref :: 2020
19 call right value ref :: 1010
20 call right value ref :: 2020
21 請按任意鍵繼續. . .
22 */

【3】常量左值引用 與 常量右值引用

(1)常量左值引用

通常情況下,右值引用是不能夠綁定到任何的左值的。比如下面的表達式就是無法通過編譯的:

int c;
int && d = c;

那么,在C++98標准中就已經出現的左值引用是否可以綁定到右值(由右值進行初始化)呢?比如:

T & e = ReturnRvalue();
const T & f = ReturnRvalue();

這樣的語句是否能夠通過編譯呢?答案是:e的初始化會導致編譯時錯誤,而f則不會。出現這樣的狀況的原因:

常量左值引用在C++98標准中開始就是個“萬能”的引用類型。它可以接受非常量左值、常量左值、右值對其進行初始化。

而且,在使用右值對其初始化的時候,常量左值引用還可以像右值引用一樣將右值的生命期延長。

不過相比於右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只讀的。

相對地,非常量左值只能接受非常量左值對其進行初始化。

(2)常量右值引用

為了語義的完整,C++11中還存在着常量右值引用。比如通過以下代碼聲明一個常量右值引用:

const T && crvalueref = ReturnRvalue();

但是,一來右值引用主要就是為了移動語義,而移動語義需要右值是可以被修改的,那么常量右值引用在移動語義中就沒有用武之處;

二來如果要引用右值且讓右值不可以更改,常量左值引用往往就足夠了。

因此,在現在的尷尬情況下,還沒有看到常量右值引用有何用處。

(3)引用類型可引用值的類型表

下面列出了在C++11中各種引用類型可以引用的值的類型:

值得注意的是,只要能夠綁定右值的引用類型,都能夠延長右值的生命期。

【4】如何判斷?

有的時候,我們可能不知道一個類型是否是引用類型,以及是左值引用還是右值引用(這在模板中比較常見)。

標准庫在<type_traits>頭文件中提供了3個模板類:is_rvalue_reference、is_lvalue_reference、is_reference,可供我們進行判斷。比如:

 1 #include <iostream>
 2 #include <type_traits>
 3 using namespace std;
 4 
 5 int main()
 6 {
 7     cout << is_reference<int>::value << endl;              // 0
 8     cout << is_rvalue_reference<string &&>::value << endl; // 1
 9     cout << is_lvalue_reference<string &> ::value << endl; // 1
10     cout << is_rvalue_reference<const string &&>::value << endl; // 1
11     cout << is_lvalue_reference<const string &> ::value << endl; // 1
12 }

我們通過模板類的成員value就可以打印出stirng && 是否是一個右值引用了。

配合C++11中的類型推導操作符decltype,我們甚至還可以對變量的類型進行判斷。

 

good good study, day day up.

順序 選擇 循環 總結


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM