就TM你叫std::move啊?

在閱讀源碼的時候,我發現有兩個標準庫的方法被頻繁使用,他們就是std::movestd::forward,因此打算一探究竟。只知道他們能做什麼是不能滿足我的,我還想只它他們爲什麼會出現。這兩個函數與C++11標準引入的新特性——右值引用和完美轉發密切相關。這裏先介紹std::move,回頭由空再去看std::forword

是什麼

右值引用,簡單的說就是一個指向右值的引用,它通過符號&&獲得,例如int&& rr = 100,這裏rr就是一個右值引用,它與其他的引用一樣,只過不是別的對象的一個移用。右值引用有兩個很重要的特性:1)如果一個對象被一個右值引用所引用的話,我們就會假設它即將被銷燬,換句話說就是不會再有別的地方會使用到這個對象;2)左值不能被右值引用所引用。因爲默認這個對象不會被使用,如果有需要,我們就可以把這個對象所擁有的資源轉移給別的對象。
這裏有必要說一下拷貝和轉移的區別。拷貝是將資源複製一遍,拷貝完成後你有一份我也有一份,而且是一模一樣的;而轉移則是將資源給出去,我們兩擁有的是同一個東西。
因此,右值引用的一個主要目的就是實現一個對象中的資源高效的轉移到另一個對象。有些時候,我們並不想對對象進行拷貝,例如出於性能的考慮,我們不想對對象進行拷貝,因爲很可能這個對象擁有的資源很大,拷貝後得到的和原來的對象一模一樣,白白花了大把時間去拷貝,何必呢?
舉個例子,我們有一個函數,這個函數接收一個對象作爲參數:

// Define a function
void foo(Bar bar) {
      // do something
}

// Function usage
Bar bar()
foo(bar);

在上面的例子中,當函數調用的時候,會發生一次拷貝,bar的拷貝構造函數會調用,創建一個新的對象並將bar拷貝後作爲實參傳遞給函數的形參。假設這個bar對象中擁有很多內存資源,而且函數調用以後bar就不會再被使用。那麼對bar的拷貝就造成了極大的浪費,最好的解決辦法就直接將bar所擁有的資源轉移給函數的形參。
我們改一改我們函數的定義:

void foo(Bar&& bar) {
      // do something
}

這回可以了吧,把形參定義成一個右值移用,這樣bar的資源就直接轉移給函數形參了。答案是不能!上面說過,右值引用有個屬性就是不能和左值綁定,只能和右值綁定,因此必須把bar變成右值。怎麼讓bar變成右值?此時,就該我們的主角std::move登場了。
std::move的作用等效於使用static_cast將一個左值強制轉換到一個右值類型。相當於告訴編譯器,這個對象已經不再使用,你可以隨便將它所擁有的資源轉移給別的對象使用。
因此,我們的函數調用就變成了下面這個樣子:

Bar bar()
foo(std::move(bar));

這樣就不會發生拷貝了。等一等,如果僅僅是爲了防止拷貝,我們使用一般的引用也是能達到目的的,例如下面這個例子:

// Define a function
void foo(Bar& bar) {
      // do something
}

// Function usage
Bar bar()
foo(bar);

這樣也能不發生拷貝,有必要大費周章還專門引入一個特性?因爲學C++的頭髮太旺盛?
當然不是,之所以引入右值引用,進而引入std::move這個函數而不直接使用一般的引用來共享資源,是爲了解決一個關鍵問題——那就是所有權問題。畢竟東西就這一份,不能說你不用了就隨便處置。就比如大家使用同一口鍋吃飯,不能說你吃飽了就把鍋砸了,我們還沒吃飽呢,這不合適。

爲什麼

使用std::move最關鍵的是實現所有權轉移,所謂的所有權,就是這個對象的狀態由誰來維護。你們明確說這口鍋你們不會再用了,怎麼處置都隨我,我吃完後我就可以放心的砸了。這個所有權有時候很重要,例如,下面的例子:

Foo* p = new Foo(10);
vec.push_back(p);

雖然語法上沒有問題,但是p的所有權很不清晰:究竟是誰來負責釋放p所佔用的資源?什麼時候釋放?列表vec被銷燬的時候p也一同被銷燬嗎?爲了解決這些問題,我們需要一個明確的所有權歸屬。

std::unique_ptr<Foo>  p (new Foo());
vec.push_back(std::move(p));

上面的例子,通過std::move,明確的將p所有權的所有權給了列表vec,這樣,就由列表決定何時釋放p所佔用的資源。那麼,是怎麼實現所有權轉移的?答案就是移動構造函數。使用std::move會讓編譯器調用移動構造函數或者移動賦值函數,在移動構造函數裏面實現所有權轉移。看下面的例子:

#include <iostream>
#include <vector>

using namespace std;

class Foo {
    public:
    Foo(int member): member_(member) { 
        ip_ = new int(member_);
    }

    Foo(Foo&& foo) {
        ip_ = foo.ip_;
        foo.ip_ = nullptr;
        cout<< "Move constructor is called." << endl;
    }
    Foo(Foo& foo) {
        cout<< "Copy constructor is called." << endl;
    }

    void show();

    ~Foo() {
        free(ip_);
        cout << "deconstruct" << endl;
    }
    
    private:
    int member_;
    int* ip_;

};

void Foo::show() {
    cout << *ip_ << endl;
}

int main(int argc, char* args[]) {
    std::vector<Foo> vec;
    Foo p(10);
    vec.push_back(std::move(p));
    return 0;
}

在移動構造函數中,新的對象拿到原有對象資源的地址後,將原有對象的指針設置爲了空,實際上原有對象在這之後可能依舊存在,但是和我們的新對象已經沒有任何關係,任何試圖通過原有對象訪問原有資源的操作還可能會引發異常。

總結

上文概括起來其實就四個字:爲了性能。std::move的作用就是將左值轉換爲右值,使得可以和右值引用綁定。而引入右值引用的目的就是解決資源高效轉移,免去不必要的拷貝操作。同時,還明確資源的所有權問題。方法就是移動構造函數或者移動賦值函數來實現。

References

[1] When should I use std::move in C++11?
[2] cppreference.com
[3] C++ Priemier 5th edition


本文首發於個人微信公衆號TensorBoy。如果你覺得內容還不錯,歡迎分享並關注我的微信公衆號TensorBoy,掃描下方二維碼獲取更多精彩原創內容!
公衆號二維碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章