1. 程式人生 > >【轉】c++之右值引用

【轉】c++之右值引用

從左值右值到右值引用

地址:https://www.cnblogs.com/inevermore/p/4029753.html

C++98中規定了左值和右值的概念,但是一般程式設計師不需要理解的過於深入,因為對於C++98,左值和右值的劃分一般用處不大,但是到了C++11,它的重要性開始顯現出來。

C++98標準明確規定:
左值是可以取得記憶體地址的變數。
非左值即為右值。

從這裡可以看出,可以執行&取地址的就是左值,其他的就是右值
這裡需要明確一點,能否被賦值不是區分C++左值和右值的區別
我們給出四個表示式:

string one("one")
; const string two("two"); string three() { return "three"; } const string four() { return "four"; }

這裡四個變量表達式,後面兩個是臨時變數,不可以取地址,所以屬於右值,前面兩個就是左值。

這裡寫出一個函式:

void test(const string &s)
{
    cout << "test(const string &s):" << s << endl;
}
//然後進行測試:
test(one);
test(two);
test
(three()); test(four());

編譯我們發現,這個test可以接受所有的變數。我們使用另一個函式做測試:

void test(string &s)
{
    cout << "test(string &s):" << s << endl;
}

然後測試發現,只有one可以通過呼叫。然後我們同時提供兩個test函式,然後我們編譯執行發現:

test(string &s):one
test(const string &s):two
test(const string &s):three
test
(const string &s):four

所以我們可以得出結論:

在C++中,const X&可以接受所有的變數
X &只可以接受普通變數

同時我們可以看出C++過載決議的一個特點:

當一個引數可以匹配多個函式時,總是匹配最精準的一個。

例如上面的one,它也可以接受const X&,但是當X&形式的引數存在時,立刻選擇後者。顯然後者是專門為其準備的,二者的語義最為符合。X&包含有一種修改語義,這與one是一致的。

引入const屬性

上面的四個表示式,我們只討論了左值和右值,我們再加上const進行討論。所以:

string one(“one”); 屬於非const左值
const string two(“two”); const左值
string three() { return “three”; } 非const右值
const string four() { return “four”; } const右值

左值右值的屬性與const是正交的。
現在引入一個問題,如果有時候需要區分四種變數,那麼該使用什麼方法?
前面的討論,我們知道X&可以用來區分one,但是剩下的三個都可以被const X&吞掉,顯然我們需要為一些變數提供一些定製版本的引數,來讓不同的變數優先選擇不同的引數。
C++11提供了右值引用來專門區分右值,我們同時提供四個函式:

void test(const string &s)
{
    cout << "test(const string &s):" << s << endl;
}

void test(string &s)
{
    cout << "test(string &s):" << s << endl;
}

void test(string &&s)
{
    cout << "test(string &&s):" << s << endl;
}

void test(const string &&s)
{
    cout << "test(const string &&s):" << s << endl;
}

我們使用C++11進行編譯,發現:

test(string &s):one
test(const string &s):two
test(string &&s):three
test(const string &&s):four

我們得出最佳匹配:

X & 匹配 非const左值
const X& 匹配 const左值
X && 匹配 非const右值
const X && 匹配 const右值

然後,我們可以採用逐個函式進行測試,發現:

X& 僅僅匹配 非const左值,這與之前的結論一致
const X& 可以匹配所有的表示式
X && 只可以匹配 非const右值
const X &&可以匹配const和非const 右值

OK,我們的問題解決,當我們需要區分右值的時候,就可以使用右值引用。
事實上,我們一般不需要const X &&,所以我們使用下列三種引數:

void test(string &s); 修改語義
void test(string &&s); 移動語義,後文介紹
void test(const string &s); 常量語義

這三種語義已經足夠,在C++98中我們只使用修改和常量語義,在C++11中,正是為了實現移動語義,我們才需要區分右值,才需要引入右值引用。

下文講C++11右值引用實現移動語義。

右值引用與移動語義

地址:https://www.cnblogs.com/inevermore/p/4029914.html

上節我們提出了右值引用,可以用來區分右值,那麼這有什麼用處?

問題來源

我們先看一個C++中被人詬病已久的問題:
我把某檔案的內容讀取到vector中,用函式如何封裝?
大部分人的做法是:

void readFile(const string &filename, vector<string> &words)
{
    words.clear();
    //read XXXXX
}

這種做法完全可行,但是程式碼風格談不上美觀,我們嘗試著這樣編寫:

vector<string> readFile(const string &filename)
{
    vector<string> ret;
    ret.push("cesfwfgw");

    //....
    //

    return ret;
}

這樣我們就可以在main中這樣呼叫:
vector<string> coll = readFile("fef.text");

但是,稍微熟悉C++的都知道,這樣在語法上會造成大量的開銷:

  • ret複製給臨時變數,該臨時變數開闢在heap上
  • 臨時變數複製給coll
  • 這中間產生兩次複製和銷燬的開銷。

如果說這個例子,可以採用開頭的程式碼解決開銷,那麼如果是一個查詢返回結果的函式,那麼我們必須這樣寫:

//這裡的開銷就無法避免了。
vector<string> queryWord(const string &word)
{
    vector<string> result;
    //XXXXX

    return result;
}

移動語義的引入

我們考慮一個生活中常見的問題(這裡參考了如何評價 C++11 的右值引用(Rvalue reference)特性?),如果把一個很重的貨物從箱子A移動到箱子B,那麼

  • 正常的做法是:開啟箱子A,把物品搬出來,移動到B,然後關上A。
  • 另一種比較奇葩的做法是:在B中複製一個物品A,然後將A中的銷燬。
  • 更奇葩的做法是:由於複製工具的侷限性,我們無法直接在B中複製,所以我們只好先在地上覆制一個物品temp,銷燬A中的物品,然後根據temp在B中再複製一份,再銷燬地上的temp。

事實上,C++98採用的就是最後一種效率極其低下的做法,這裡的關鍵在於,C++98沒有很好的區分“copy”和“move”語義
上述問題中,我們明確提出移動A到B中,但是C++98由於移動語義的缺失,只好採用copy的方式完成。
我們再回到開頭的問題中:

vector<string> readFile(const string &filename)
{
    vector<string> ret;
    ret.push("cesfwfgw");

    //....
    //

    return ret;
}

這裡我們必須看到一點,在完成函式呼叫後,ret就要被銷燬,所以我們想到一個主意,不是把ret中的內容複製給main中的coll,而是將ret中的內容偷到coll中,然後將ret悄悄的銷燬。
這樣是可行的,因為ret的生命週期很短。

哪些可以偷?

現在問題來了,C++中哪些可以偷,哪些不能?
我們回顧上一節提到的四個表示式:

string one("one");
const string two("two");
string three() { return "three"; }
const string four() { return "four"; }

顯然,one和two生命週期較長,不能偷。four具有const屬性,拒絕被偷。
那麼three是可以被偷取的,因為它是臨時變數,又沒有const屬性。
所以,C++中的非const右值,和移動語義完全匹配。上節我們提出用右值引用區分右值,正是為了解決哪些可以偷的問題!

OK,我們的思路已經很清晰了:

1.為了解決返回物件開銷問題,我們提出“偷取”,而不是複製物件
2.我們面臨哪些能偷,哪些不能偷的問題。
3.右值可以偷取,所以我們如何區分右值?
4.我們引入右值引用X &&來區分右值。

這就是右值引用的來源。

如果一個變數不是右值,但是我們又需要偷取,那麼我們可以採用std::move函式,將其強制轉化為右值引用。

例如:

void test(string name)
{
    string temp(std::move(name));
    // XXXXXX
    //**注意,被偷取之後的name無法繼續使用,所以move函式不可以隨意使用**。
}

帶來的影響

那麼,右值引用帶來哪些改變呢?
首先是類的成員函式賦值,看下面程式碼:

class People
{
public:
    People() 
    {
        cout << "People()" << endl;
    }
    People(const string &name)
    : name_(name)
    {
        cout << "People(const string &name)" << endl;
    }
    //這裡name賦值,我們相對於C++98,提供了一個右值函式,將name的值移動給name_。
    People(string &&name)
    : name_(name)
    {
        cout << "People(string &&name)" << endl;
    }

private:
    string name_;
};

事實上,上面的兩個函式可以合成一個:

class People
{
public:
    People() 
    {
        cout << "People()" << endl;
    }

    People(string name)
    : name_(std::move(name))
    {
        cout << "People(string name)" << endl;
    }

private:
    string name_;
};

這裡注意,上面的name採用傳值,並沒有帶來開銷,因為:

  • 如果name傳入的是一個右值,那麼name本身採用移動構造,開銷比複製小很多,相當於People(string &&name)
  • 如果name傳入的其他值,那麼name是複製構造,然後移動給name_,也沒有增加額外的開銷。

對於建構函式,除了提供複製建構函式,還需要移動建構函式。如下:

class People
{
public:
    People() 
    {
        cout << "People()" << endl;
    }

    People(string name)
    : name_(std::move(name))
    {
        cout << "People(string name)" << endl;
    }

    People(const People &p)
    : name_(p.name_)
    {
        cout << "People(const People &p)" << endl;
    }
    People(People &&p)
    : name_(std::move(p.name_))
    {
        cout << "People(People &&p)" << endl;
    }

private:
    string name_;
};

注意在最後一個People(People &&p)中,移動p內的name時,必須顯式使用move,來強制移動name成員。

同樣,還有移動賦值運算子。

另外,在C++98中,容器內的元素必須具備值語義,現在則不同,元素具備移動能力即可,後文我們在智慧指標系列會提到unique_ptr,它可以放入vector中,但是不具有複製和賦值能力。
其他的影響請參考:如何評價 C++11 的右值引用(Rvalue reference)特性?
下文通過一個string的模擬實現,演示右值引用的使用。

使用C++11編寫string類以及“異常安全”的=運算子

地址:https://www.cnblogs.com/inevermore/p/4032008.html
前面兩節,說明了右值引用和它的作用。下面通過一個string類的編寫,來說明右值引用的使用。

相對於C++98,主要是多了移動建構函式和移動賦值運算子

class String
{
public:
    String();
    String(const char *s); //轉化語義
    String(const String &s);
    String(String &&s);
    ~String();

    String &operator=(const String &s);
    String &operator=(String &&s);

    friend ostream &operator<<(ostream &os, const String &s)
    {
        return os << s.data_;
    }
private:
    char *data_;
};

下面依次實現每個函式。
第一個是預設建構函式:

String::String()
:data_(new char[1])
{
    *data_ = 0;
    cout << "default" << endl;
}

然後是char*版本的建構函式:

String::String(const char *s)
:data_(new char[strlen(s) + 1])
{
    ::strcpy(data_, s);
    cout << "char *" << endl;
}

重點來了,我們提供移動建構函式:

String::String(String &&s)
:data_(s.data_) //為什麼這裡可以這樣
{
    cout << "move construct" << endl;
    s.data_ = NULL; //防止釋放data
}

這裡最重要的一點就是要把s的data置為NULL,因為s是個右值,馬上就要析構。這樣就成功實現了偷取s的內容

解構函式:

String::~String()
{
    delete[] data_;
}

下面我們提供賦值運算子,這裡注意一點:
一是處理自我賦值,二是要返回自身引用。

String &String::operator=(const String &s)
{
    if(this != &s)
    {
        delete[] data_;
        data_ = new char[strlen(s.data_) + 1];
        ::strcpy(data_, s.data_);
    }
    return *this;
}

String &String::operator=(String &&s)
{
    if(this != &s)
    {
        cout << "move assignment" << endl;
        delete[] data_;
        data_ = s.data_;
        s.data_ = NULL;
    }
    return *this;
}

後面的移動建構函式,依然要把s的data置為NULL。
上面兩個函式看似正確,但是沒有處理髮生異常的情況,如果new時發生異常,但是此時原本的data已經被delete,造成錯誤。

如何解決?
我們提供一個swap函式:

void String::swap(String &s)
{
    std::swap(data_, s.data_);
}

一種好的處理方案是:

String &String::operator=(const String &s)
{
    String temp(s);
    swap(temp);

    return *this;
}

String &String::operator=(String &&s)
{
    String temp(s);
    swap(temp);

    return *this;
}

這樣,即使生成temp時發生異常,也對自身沒有影響。

注意這裡沒有處理自我賦值,因為自我賦值發生的情況實際比較少,而之前的程式碼第一行是delete,則必須處理自我賦值。

上面兩個賦值運算子可以直接合為一個:

String &String::operator=(String s)
{
    swap(s);

    return *this;
}

事實上,我們在前面也提到過,除了建構函式之外,X &x和X &&型別的函式,可以合二為一為X x,採用傳值。
這樣,我們的最後一個實現,保證了異常安全。

int main(int argc, char const *argv[])
{
    String s("foo");
    String s2(s);
    //String s3(std::move(String("bar")));
    String s3(String("bar")); //編譯器優化 直接使用char*
    cout << s3 << endl;

    s3 = s;
    s3 = String("hello");
    cout << s3 << endl;
    s3 = std::move(s2);
    cout << s3 << endl;

    return 0;
}

注意:

String s3(String(“bar”));

會被編譯器優化為

String s3(“bar”)

可以顯式使用:

String s3(std::move(String(“bar”)));