定義###
早期的C給出的定義:左值是一個表達式,可能出現在賦值操作的左邊或右邊,但右值只能出現在右邊。比如:
<pre>
a * b = 42; // 編譯錯誤, 說明 a * b 不是左值
</pre>
因為上面的定義實在太模糊,導致左值和右值很難被理解,下面給出的定義,更簡單更好理解:左值(lvalue)是一個表達式,它表示一個可被標識的(變量或對象的)內存位置,并且允許使用&操作符來獲取這塊內存的地址。如果一個表達式不是左值,那它就被定義為右值。
<pre>
int i = 42;
i = 43;
int* p = &i; // ok, i 是左值
int& foo();
foo() = 42; // ok, foo() 是左值
int* p1 = &foo(); // ok, foo() 是左值
int foobar();
int j = 0;
j = foobar(); // ok, foobar() 是右值
int* p2 = &foobar(); // 錯誤,不能獲取右值的地址
j = 42; // ok, 42 是右值
</pre>
左值與右值之間的轉換###
一般上講,對象之間的運算,對象是以右值的形式參與的。比如二元運算符+兩邊的參數以右值傳入,加后的返回結果也是右值:
<pre>
int a = 1; // a 是左值
int b = 2; // b 是左值
int c = a + b; // a和b自動轉換為右值求和
</pre>
那些表示數組、函數和非完整類型的左值是不能轉換為右值的,因為無法對那些類型進行求值。incomplete types指的是類型定義不完整,只能用指針形式聲明的類型,在頭文件中經常會使用。
左值引用###
C++中可以使用&符定義引用,如果一個左值同時是引用,就稱為“左值引用”,如:
<pre>
std::string s;
std::string& sref = s; //sref為左值引用
</pre>
非const左值引用不能使用右值對其賦值
<pre>
std::string& r = std::string();//錯誤!std::string()產生一個臨時對象,為右值
</pre>
假設可以的話,就會遇到一個問題:如何修改右值的值?因為引用是可以后續被賦值的。根據上面的定義,右值連可被獲取的內存地址都沒有,也就談不上對其進行賦值。
但const左值引用不一樣,因為常量不能被修改,也就不存在上面的問題:
<pre>
const std::string& r = std::string(); //可以
</pre>
我們經常使用const左值引用作為函數的參數類型,可以減少不必要的對象復制:
<pre>
class MyString
{
public:
MyString &MyString(string& s); //參數類型為左值引用
};
int main()
{
MyString s1("XXX"); //錯誤
MyString s2(string("XXXX")); //同上,右值不能賦值給左值引用
}
</pre>
帶CV限定符(CV-qualified)的右值###
C++標準中關于左值轉右值的討論,有這樣一段話:
類型為T的左值(非函數、非數組類型)可以被轉換為右值。如果T不是類(class)類型,轉換后的右值的類型將為不帶CV限定符的T類型,否則轉換后的右值的類型為T。
什么是CV限定符?如果變量聲明時類型前帶有const或volatile,就說此變量類型具有CV限定符。
在C中,右值永遠沒有CV限定符,而C++中的類類型的右值可以有CV限定符,看下面代碼:
<pre>
class A
{
public:
void foo() const { std::cout << "A::foo() const\n"; }
void foo() { std::cout << "A::foo()\n"; }
};
A bar() { return A(); } //返回臨時對象,為右值
const A cbar() { return A(); } //返回帶const的右值(帶CV限定符)
int main()
{
bar().foo(); // 非const對象調用A::foo()的非const版本
cbar().foo(); // const對象調用A::foo()的const版本
}
</pre>
也就是說,如果是類類型,從左值轉為右值時,它的CV限定符會被保留。這里就不給出示例代碼了。
右值引用(C++11)###
右值引用及其相關的move語義是C++11新引入的最強大的特性之一。前文說到,左值(非const)可以被修改(賦值),但右值不能。但C++11引入的右值引用特性,打破了這個限制,允許我們獲取右值的引用,并修改之。讓我們先看點代碼: 定義一個類Intvec及其賦值操作符重載函數如下:
<pre>
class Intvec
{
public:
...
Intvec& operator=(const Intvec& other)
{
log("copy assignment operator");
Intvec tmp(other); //構造一個臨時對象,因為other為const,不能被修改
std::swap(m_size, tmp.m_size);
std::swap(m_data, tmp.m_data);
//跟臨時對象交換值,臨時對象晰構時會delete [] m_data
return this;
}
private:
size_t m_size;
int m_data; //存放int數組,構造時動態分配
};
</pre>
代碼要點:
- 代碼使用了copy-swap策略,即先分配資源再更改自身狀態,這樣可以保證當資源分配失敗的時候,自身能夠維持原先狀態,《高效C++》有條規則描述這個主題。所以先根據other拷貝構造一個臨時對象tmp,然后與tmp進行swap,m_data交換給了tmp之后,也會隨著tmp的晰構而被釋放。
- 之所以把other聲明為const,有兩個理由,其一是賦值操作不應該更改other,其二是可以傳入一個右值。其實這樣的聲明隨處可見。
假設現有類型為Intvec的對象v,用一個新對象給它賦值:
<pre>
v = Intvec(33);
</pre>
這句代碼合法,它構造一個臨時對象,為右值,傳入到Intvec的賦值運算符重載函數中。這個代碼是可以工作,而且通常情況下都比較高效。但是如果Intvec里包含某些m_handle成員,創建和釋放m_handle比較昂貴,那么拷貝構造越少越好。這種情況,我們設想一下,如果v能跟Intvec(33)臨時對象直接進行內部數據交換,而不需要在重載函數里使用Intvec tmp(other);構造一個新對象出來swap,那該有多好!
如你所料,C++11引入的“右值引用”和“move語義”就可以實現這個目標,新的語法很簡單,我們重載一個新的賦值操作運算符函數:
<pre>
Intvec& operator=(Intvec&& other)
{
log("move assignment operator");
std::swap(m_size, other.m_size);
std::swap(m_data, other.m_data);
return *this;
}
</pre>
對于v = Intvec(33);這種寫法就會調用此版本的重載函數(即傳入一個右值)。
&&語法聲明右值引用,表示一個指向右值的引用,通過這個引用,可以修改右值。
以上就是關于右值引用的一個簡單的示例,實際上右值引用是一個復雜的主題,在實際應用中還有很多場景要考慮。
原文鏈接