天天看點

C++11 右值引用&&

一、 新特性的目的

右值引用 (Rvalue Referene) 是 C++ 新标準 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它實作了轉移語義 (Move Sementics) 和精确傳遞 (Perfect Forwarding)。它的主要目的有兩個方面:

1. 消除兩個對象互動時不必要的對象拷貝,節省運算存儲資源,提高效率。

2. 能夠更簡潔明确地定義泛型函數。

二、 何為右值

C++( 包括 C) 中所有的表達式和變量要麼是左值,要麼是右值。通俗的左值的定義就是非臨時對象,那些可以在多條語句中使用的對象。 所有的變量都滿足這個定義,在多條代碼中都可以使用,都是左值。 右值是指臨時的對象,它們隻在目前的語句中有效。

int i = 0;  // 在這條語句中,i 是左值,0 是臨時值,就是右值。

在C++11之前,右值是不能被引用的,如:

int &a = 1;   // error C2440: “初始化”: 無法從“int”轉換為“int &”

我們最多隻能用常量引用來綁定一個右值,如:

const int &a = 1;

在C++11中,我們可以引用右值,使用&&來實作:

int &&a = 1;

三、 應用場景

有如下string類,實作了拷貝構造函數和指派運算符重載。

class MyString {

private:

    char* _data;

    size_t   _len;

    void _init_data(const char *s) {

        _data = new char[_len + 1];

        memcpy(_data, s, _len);

        _data[_len] = '\0';

    }

public:

    MyString() {

        _data = NULL;

        _len = 0;

    }

    MyString(const char* p) {

        _len = strlen(p);

        _init_data(p);

    }

    MyString(const MyString& str) {

        _len = str._len;

        _init_data(str._data);

        std::cout << "Copy Constructor is called! source: " << str._data << std::endl;

    }

    MyString& operator=(const MyString& str) {

        if (this != &str) {

            _len = str._len;

            _init_data(str._data);

        }

        std::cout << "Copy Assignment is called! source: " << str._data << std::endl;

        return *this;

    }

    virtual ~MyString() {

        if (_data != NULL) {

            std::cout << "Destructor is called! " << std::endl;

            free(_data);

        }

    }

};

int main() {

    MyString a;

    a = MyString("Hello");

    std::vector<MyString> vec;

    vec.push_back(MyString("World"));

}

運作結果:

Copy Assignment is called! source: Hello

Destructor is called!

Copy Constructor is called! source: World

Destructor is called!

Destructor is called!

Destructor is called!

總共執行了2次拷貝,MyString("Hello")和MyString("World")都是臨時對象,臨時對象被使用完之後會被立即析構,在析構函數中free掉申請的記憶體資源。 如果能夠直接使用臨時對象已經申請的資源,并在其析構函數中取消對資源的釋放,這樣既能節省資源,有能節省資源申請和釋放的時間。 這正是定義轉移語義的目的。

通過加入定義轉移構造函數和轉移指派操作符重載來實作右值引用(即複用臨時對象):

    MyString(MyString&& str) {

        std::cout << "Move Constructor is called! source: " << str._data << std::endl;

        _len = str._len;

        _data = str._data;

        str._len = 0;

        str._data = NULL;   // ! 防止在析構函數中将記憶體釋放掉

    }

    MyString& operator=(MyString&& str) {

        std::cout << "Move Assignment is called! source: " << str._data << std::endl;

        if (this != &str) {

            _len = str._len;

            _data = str._data;

            str._len = 0;

            str._data = NULL;  // ! 防止在析構函數中将記憶體釋放掉

        }

        return *this;

    }

運作結果:

Move Assignment is called! source: Hello

Move Constructor is called! source: World

Destructor is called!

Destructor is called!

需要注意的是:右值引用并不能阻止編譯器在臨時對象使用完之後将其釋放掉的事實,是以轉移構造函數和轉移指派操作符重載函數 中都将_data指派為了NULL,而且析構函數中保證了_data != NULL才會釋放。

四、 标準庫函數 std::move

既然編譯器隻對右值引用才能調用轉移構造函數和轉移指派函數,又因為所有命名對象都隻能是左值引用。 在這樣的條件了,如果已知一個命名對象不再被使用而想對它調用轉移構造函數和轉移指派函數,也就是把一個左值引用當做右值引用來使用,怎麼做呢?标準庫提供了函數 std::move,這個函數以非常簡單的方式将左值引用轉換為右值引用。

void ProcessValue(int& i) {

    std::cout << "LValue processed: " << i << std::endl;

}

void ProcessValue(int&& i) {

    std::cout << "RValue processed: " << i << std::endl;

}

int main() {

    int a = 0;

    ProcessValue(a);

    ProcessValue(std::move(a));

}

運作結果:

LValue processed: 0

RValue processed: 0

std::move在提高 swap 函數的的性能上非常有幫助,一般來說,swap函數的通用定義如下:

template <class T>

void swap(T& a, T& b)

{

    T tmp(a);   // copy a to tmp

    a = b;      // copy b to a

    b = tmp;    // copy tmp to b

}

有了std::move,再結合右值引用,就可以避免不必要的拷貝了。 swap函數的定義變為 :

template <class T>

void swap(T& a, T& b)

{

    T tmp(std::move(a)); // move a to tmp

    a = std::move(b);    // move b to a

    b = std::move(tmp);  // move tmp to b

}

可以使用第三節中的MyString類進行測試:

int main() {

    MyString a("a");

    MyString b("b");

    swap(a, b);

    return 0;

五、 精确傳遞(Perfect Forwarding)

精确傳遞就是在參數傳遞過程中,所有這些屬性和參數值都不能改變。在泛型函數中,這樣的需求非常普遍。 舉例說明比較好了解。

forward_value函數隻有一個參數val,定義如下:

template <typename T>

void forward_value(const T& val) {

    process_value(val);

}

template <typename T>

void forward_value(T& val) {

    process_value(val);

}

函數 forward_value 為每一個參數必須重載兩種類型,T& 和 const T&,否則,下面四種不同類型參數的調用中就不能同時滿足:

int a = 0;

const int &b = 1;

forward_value(a); // int&

forward_value(b); // const int&

forward_value(2); // int&

對于一個參數就要重載兩次,也就是函數重載的次數和參數的個數是一個正比的關系。這個函數的定義次數對于程式員來說,是非常低效的。我們看看右值引用如何幫助我們解決這個問題:

template <typename T>

void forward_value(T&& val) {

    process_value(val);

}

隻需要定義一次,接受一個右值引用的參數,就能夠将所有的參數類型原封不動的傳遞給目标函數。

經測試,VS2015已經支援右值引用&&。

繼續閱讀