C++11中的右值引用和移動語義

右值引用是C++11中新增的一種新的引用類型,它可以通過減少內存的重複申請、拷貝和釋放,有效的提高C++程序的性能。理解什麼是右值引用,首先要理解C++11中的lvalue、rvalue、xvalue,詳情請參考:C++11 中的左值、右值和將亡值.

右值引用和普通的非常量引用的唯一區別是,非常量引用(non-const reference)通常綁定一個非臨時變量,而右值引用通常綁定一個臨時變量。

一個常量引用(const reference)既可以綁定臨時變量,也可以綁定非臨時變量。正是因爲常量引用(const reference)的這個特性,當類的聲明中沒有移動構造函數時,即A(A&& rhs){},傳遞臨時變量,任然可以調用A(const A& rsh){}構造函數進行構造。

右值引用,避免深拷貝

一個深拷貝的例子

class A
{
public:
    A() : m_ptr(new int(0))
    {
        cout << "construct" << endl;
    }
    A(const A& rhs) : m_ptr(new int(*rhs.m_ptr)) // 拷貝構造函數
    {
        cout << "copy construct" << endl;
    }
    ~A()
    {
        cout << "destruct" << endl;
        delete m_ptr;
    }
private:
    int* m_ptr;
};

A Get(bool flag)
{
    A a, b;
    if (flag)
    {
        return a;
    }
    return b;
}

int main()
{
    A a = Get(false);//調用拷貝構造函數
    return 0;
}

執行結果:
construct
construct
copy construct
destruct
destruct
destruct

問題:上述例子中Get函數會返回臨時變量,然後通過這臨時變量拷貝構造一個新的對象a,臨時變量在拷貝構造完成之後就銷燬了,如果堆內存很大,這個拷貝構造就比較消耗時間,帶來性能的損耗,是否有方法避免臨時對象的拷貝構造呢?
答案:右值引用

改進深拷貝的例子

在class A中添加如下移動構造函數

A(A&& rhs) : m_ptr(rhs.m_ptr) //移動構造函數
{
  rhs.m_ptr = nullptr;
  cout << "move construct" << endl;
}

執行結果:
construct
construct
move construct
destruct
destruct
destruct

  • 改進之後,不再調用拷貝構造函數,而是調用了移動構造函數(move construct)。從移動構造函數的實現可以看到,它的參數是一個右值引用類型的參數T&&,這裏沒有深拷貝,只有淺拷貝,這樣就避免了對臨時對象的深拷貝,和資源的重複申請和釋放,提高了性能;
  • Get(false)函數返回的是一個右值,因此調用了參數類型爲A&&的移動構造函數;
  • 移動構造函數只是將臨時對象的資源做了淺拷貝,從而避免了深拷貝的開銷,提高了性能;
  • 這就是所謂的移動語義(move semantic),右值引用的一個重要目的就是支持移動語義;

右值引用主要爲了解決下面兩個問題

  1. 移動語義 (move semantic)
  2. 完美轉發 (perfect forwarding)

移動語義(move semantic)

問題: 移動語義是通過右值引用來匹配臨時值的,那麼,普通的左值是否也能借助移動語義來優化性能呢?
- C++11 爲了解決上述問題,提供了std::move方法來將左值轉換爲右值,從而方便應用移動語義;
- move實際上並不能移動任何東西,他唯一的功能是將一個左值強制轉換爲一個右值引用,使我們可以通過右值引用使用該值;
- It’s confusing, because “move” is a verb; it sounds like it should do something. But in fact it’s just a cast, just like const_cast or reinterpret_cast. Like those casts, std::move just lets us use an object in a different way; on its own, it doesn’t do anything.

模擬string中移動構造函數的例子

  class string {
 public:
  string(const string& other);  // Copy constructor, exists pre C++11

  string(string&& other) {      // Move constructor, new in C++11
    length = other.length;
    capacity = other.capacity;
    data = other.data;
    other.data = nullptr;
  }

 private:
  size_t length;
  size_t capacity;
  const char* data;
};
  • 移動構造函數的處理流程:① 獲取臨時變量other中的指針、拷貝其他非指針成員,② 將臨時對象的指針設置爲空,other.data = nullptr;。若不將other.data設置爲空,other.data 將被釋放兩次,導致程序異常。下面是使用string的例子,當使用臨時變量構造時,調用移動構造函數。
string a(get_string());  // move constructor
string b(a);             // copy constructor
string c(std::move(b));  // move constructor

注意:若string中沒有移動構造函數string(string&& other),上述的string c(std::move(b));仍然將調用拷貝構造函數進行構造,因爲string(const string& other)的參數是const reference。

模擬string中的賦值操作符

通過上面的分析,你肯定發現這種寫法也可以應用到string的賦值操作符上,代碼如下:

class string {
 public:
  string& operator=(const string& other); // Copy assn operator, pre C++11
  string& operator=(string&& other) {     // Move assn operator, new in C++11
    length = other.length;
    capacity = other.capacity;
    delete data;  // OK even if data is null
    data = other.data;
    other.data = nullptr;
    return *this;
  }
};

string a, b;
a = get_string();  // Move assignment
a = b;             // Copy assignment
a = std::move(b);  // Move assignment

C++11中,編譯器將會在類中自動添加移動構造函數和移動賦值操作符,因此C++11中,聲明一個類,編譯器會默認添加構造函數、拷貝構造函數、析構函數、移動構造函數和移動賦值操作,號稱類中的”5駕馬車”,而不是C++11之前的“3駕馬車”。

若用戶自定義拷貝構造函數,編譯器將不會自動添加移動構造函數和移動賦值操作。可以在這些函數之後添加 = delete,顯示的阻止編譯器自動添加對應的函數。

完美轉發 (perfect forwarding)

轉下一篇C++11 模板推導、引用摺疊和完美轉發

參考

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