c++中的左值與右值

左值(lvalue)和右值(rvalue)是 c/c++ 中一個比較晦澀基礎的概念,很多寫了好久c/c++的人甚至沒有聽過這個名字,但這個概念到了 c++11 後卻變得十分重要,它們是理解 move/forward 等新語義的基礎。php

左值右值的定義

左值與右值這兩概念是從 c 中傳承而來的,在 c 中,左值指的是既可以出如今等號左邊也能出如今等號右邊的變量(或表達式),右值指的則是隻能出如今等號右邊的變量(或表達式).html

int a; int b; a = 3; b = 4; a = b; b = a; // 如下寫法不合法。
3 = a; a+b = 4;

在 c 語言中,一般來講有名字的變量就是左值(如上面例子中的 a, b),而由運算操做(加減乘除,函數調用返回值等)所產生的中間結果(沒有名字)就是右值,如上的 3 + 4, a + b 等。咱們暫且能夠認爲:左值就是在程序中可以尋值的東西,右值就是無法取到它的地址的東西(不徹底準確),但如上概念到了 c++ 中,就變得稍有不一樣。具體來講,在 c++ 中,每個表達式都會產生一個左值,或者右值,相應的,該表達式也就被稱做「左值表達式", "右值表達式"。對於基本數據類型來講(primitive types),左值右值的概念和 c 沒有太多不一樣,不一樣的地方在於自定義的類型,並且這種不一樣比較容易讓人混淆:c++

1) 對於基礎類型,右值是不可被修改的(non-modifiable),也不可被 const, volatile 所修飾(cv-qualitification ignored)函數

2) 對於自定義的類型(user-defined types),右值卻容許經過它的成員函數進行修改。this

對於 1),這和 c 是一致的,2) 倒是 C++ 中所獨有, 所以,若是你看到 C++ 中以下的寫法,千萬不要驚訝:spa

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 類型,所以經過調用右值的成員函數,也就可能會修改了該右值,done!設計

左值引用,右值引用

關於右值,在 c++11 之前有一個十分值得關注的語言的特性:右值能被 const 類型的引用所指向,因此以下代碼是合法的。c++11

const cs& ref = get_cs();

並且準確地說,右值只能被 const 類型的 reference 所指向,非 const 的引用則是非法的:code

// error  cs& ref = get_cs();

當一個右值被 const 引用指向時,它的生命週期就被延長了,這個用法我在前面一篇博客裏講到過它的相關應用。其中暗藏的邏輯其實就是:右值不能當成左值使用(但左值能夠當成右值使用)。另外值得注意的是,對於前面提到的右值的兩個特性:htm

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() 等,這兒先賣個關子,咱們下次再講。

[參考]

http://accu.org/index.php/journals/227

相關文章
相關標籤/搜索