左右值的概念
C++中左值(lvalue)和右值(rvalue)在C++11后變得尤為重要,是理解 move、forward等新語義的基礎。
左值和右值這兩個概念是從C中傳承過來的。
左值指的是:既能出現在等號左邊,也能出現在等號右邊的變量/表達式;
右值指的是:只能出現在等號右邊的變量/表達式。
如:
int k;
k = 3;
------
int a;
int b;
a = 3;
b = 4;
a = b;
b = a;
------
// 以下寫法不合法
3 = a;
a+b = 4;
變量k就是左值,常量3就是右值。
在 c 語言中,通常來說有名字的變量就是左值 (如上面例子中的 a, b),而由運算操作 (加減乘除,函數調用返回值等) 所產生的中間結果 (沒有名字) 就是右值,如上的 3 + 4, a + b 等。我們暫且可以認為:左值就是在程序中能夠尋值的東西,右值就是沒法取到它的地址的東西 (不完全准確),
但如上概念到了 c++ 中,就變得稍有不同。具體來說,在 c++ 中,每一個表達式都會產生一個左值,或者右值,相應的,該表達式也就被稱作 “左值表達式", "右值表達式"。對於基本數據類型(primitive types) 來說,左值右值的概念和 c 沒有太多不同,不同的地方在於自定義的類型,而且這種不同容易讓人混淆:
(1)對於基礎類型,右值是不可被修改的 (non-modifiable),也不可被 const, volatile 所修飾 (cv-qualitification ignored)
(2)對於自定義的類型 (user-defined types),右值卻允許通過它的成員函數進行修改。
對於(1),這和 c 是一致的,(2)卻是 C++ 中所獨有。
因此,如果你看到 C++ 中如下的寫法,千萬不要驚訝:
class cs
{
public:
cs(int i): i_(i) { cout << "cs(" << i <<") constructor!" << endl; }
~cs() { cout << "cs destructor,i(" << i_ << ")" << endl; }
cs& operator=(const cs& other)
{
i_ = other.i_;
cout << "cs operator=()" << endl;
return *this;
}
int get_i() const { return i_; }
void change(int i) { i_ = i; }
private:
int i_;
};
cs get_cs()
{
static int i = 0;
return cs(i++);
}
int main()
{
// 合法
(get_cs() = cs(2)).change(323);
get_cs() = cs(2);// operator=()
get_cs().change(32);
return 0;
}
這個特性看起來多少有些奇怪,因為通常來說,自定義類型應該設計得和內置類型盡量一樣 (所謂 value type,value semantic),但允許成員函數改變右值這個特性卻有意無意使得自定義類型特殊化了。對此,我們其實可以這樣想,也許會好理解點:自定義類型允許有成員函數,而通過右值調用成員函數是被允許的,但成員函數有可能不是 const 類型,因此通過調用右值的成員函數,也就可能會修改了該右值。
左右值的性質及區別
左值是可尋址的變量/表達式,有持久性;
右值一般是不可尋址的變量/表達式,亦或是表達式求值中創建的無名臨時對象,短暫性的。但是在C++11中,自定義類型允許有成員函數,通過右值可以調用成員函數,從而改變右值。
左值和右值的主要區別之一就是C語言中(左值可以被修改,而右值不能);C++11中(左值可以被修改,而右值一般不能,除非自定義類型,右值調用成員函數從而改變)。
此外,右值不能當成左值使用,但左值可以當成右值使用。
左右值引用
左值引用:引用一個對象;
右值引用:就是必須綁到右值的引用,C++11中,右值引用可以實現 “移動語義”,通過&&獲得右值引用。
int x = 6; // x是左值,6是右值
int &y = x; // 左值引用,y引用x
int &a = x * 6; //
const int &b = x * 6;
int &&c = x*6;
int &&d = x;
關於右值,在 c++11 以前有一個十分值得關注的語言的特性:右值能被 const 類型的引用所指向,所以如下代碼是合法的。
const cs& ref = get_cs();
而且准確地說,右值只能被 const 類型的 reference 所指向,非 const 的引用則是非法的:
// error
cs& ref = get_cs();
當一個右值被 const 引用指向時,它的生命周期就被延長了,這個用法我在前面一篇博客里講到過它的相關應用。其中暗藏的邏輯其實就是:右值不能當成左值使用 (但左值可以當成右值使用)。另外值得注意的是,對於前面提到的右值的兩個特性:
1) 允許調用成員函數。
2) 只能被 const reference 指向。
它們導致了一些比較有意思的結果,比如:
void func(cs& c)
{
cout << "c:" << c.get_i() << endl;
}
//error
func(get_cs());
//正確
func(get_cs() = get_cs());
其中:func(get_cs() = get_cs()); 能夠被正常編譯執行的原因就在於,cs 的成員函數 operator=() 返回的是 cs&!不允許非 const reference 引用 rvalue 並不是完美的,它事實上也引起了一些問題,比如說拷貝構造函數的接口不一致了,這是什么意思呢?
class cs
{
public:
cs& operator=(const cs& c);
};
// 另一種寫法
class cs2
{
public:
cs2& operator=(cs2& c);
};
上面兩種寫法的不同之處就在於參數,一個是 const reference,一個是非 const。對於自定義類型的參數,通常來說,如果函數不需要修改傳進來的參數,我們往往就按 const reference 的寫法,但對於 copy constructor 來說,它經常是需要修改參數的值,比如 auto_ptr。
// 類似auto_ptr
class auto_ptr
{
public:
auto_ptr(auto_tr& p)
{
ptr_ = p.ptr_;
p.ptr_ = NULL;
}
private:
void* ptr_;
};
所以,對於 auto_ptr 來說,它的 copy constructor 的參數類型是 non const reference。有些情況下,這種寫法應該被鼓勵,畢竟 non const reference 比 const reference 更能靈活應對各種情況,從而保持一致的接口類型,當然也有代價,參數的語義表達不准確了。除此更大的問題是如果拷貝構造函數寫成這樣子,卻又對 rvalue 的使用帶來了極大的不變,如前面所講的例子,rvalue 不能被 non const reference 所引用,所以像 auto_ptr 的這樣的類的 copy constructor 就不能接受 rvalue.
// 錯誤
auto_ptr p(get_ptr());
// operator=() 同理,錯誤。
auto_ptr p = get_ptr();
這也是 auto_ptr 很不好用的原因之一,為了解決這個問題,c++11 中引入了一種新的引用類型,該種引用是專門用來指向 rvalue 的,有了這種新類型,對 lvalue 和 rvalue 的引用就能明確區分開來了。因為有了這種新的類型,接着就引出了 c++11 中新的語義,move(), forward() 等,這兒先賣個關子,我們下次再講。