(原創)C++11改進我們的程序之右值引用


本次主要講c++11中的右值引用,后面還會講到右值引用如何結合std::move優化我們的程序。

c++11增加了一個新的類型,稱作右值引用(R-value reference),標記為T &&,說到右值引用類型之前先要了解什么是左值和右值。
左值具名,對應指定內存域,可訪問;右值不具名,不對應內存域,不可訪問。臨時對像是右值。左值可處於等號左邊,右值只能放在等號右邊。區分表達式的左右值屬性有一個簡便方法:若可對表達式用 & 符取址,則為左值,否則為右值。
1.簡單的賦值語句
如:int i = 0;
在這條語句中,i 是左值,0 是臨時值,就是右值。在下面的代碼中,i 可以被引用,0 就不可以了。立即數都是右值。
2.右值也可以出現在賦值表達式的左邊,但是不能作為賦值的對象,因為右值只在當前語句有效,賦值沒有意義。
如:((i>0) ? i : j) = 1;
在這個例子中,0 作為右值出現在了”=”的左邊。但是賦值對象是 i 或者 j,都是左值。
在 C++11 之前,右值是不能被引用的,最大限度就是用常量引用綁定一個右值,如 :
const int &a = 1;
在這種情況下,右值不能被修改的。但是實際上右值是可以被修改的,既然右值可以被修改,那么就可以實現右值引用。右值引用能夠方便地解決實際工程中的問題。

int && a = 1; //&&為右值引用

&&的特性

  實際上T&&並不是一定表示右值引用,它的引用類型是未定的,即可能是左值有可能是右值。看看這個例子:

template<typename T>
void f(T&& param);

f(10); //10是右值
int x = 10;
f(x); //x是左值

  從這個例子可以看出,param有時是左值引用,有時是右值引用,它在上面的例子中&&實際上是一個未定的引用類型。這個未定的引用類型被scott meyers稱為universal references(可以認為它是種通用的引用類型),它必須被初始化,它是左值應用還是右值引用取決於它的初始化,如果&&被一個左值初始化的話,它就是一個左值引用;如果它被一個右值初始化的話,它就是一個右值引用。

&&為universal references時的唯一條件是有類型推斷發生。

template<typename T>
void f(T&& param); //這里T的類型需要推導,所以&&是一個universal references

template<typename T>
class Test {
...
Test(Test&& rhs); // 已經定義了一個特定的類型, 沒有類型推斷
... // && 是一個右值引用
};

void f(Test&& param); // 已經定義了一個確定的類型, 沒有類型推斷,&& 是一個右值引用

再看一個復雜一點的例子

template<typename T>
void f(std::vector<T>&& param); 

這里既有推斷類型T又有確定類型vector,那么這個param到底是什么類型呢?
它是右值引用類型,因為在調用這個函數之前,這個vector<T>中的推斷類型已經確定了,所以到調用f時沒有類型推斷了。

再看看這個例子:

template<typename T>
void f(const T&& param);

這個param是universal references嗎?錯,它是右值引用類型,也許會迷糊,T不是推斷類型嗎,怎么會是右值引用類型。其實還有一條規則:universal references僅僅在T&&下發生,任何一點附加條件都會使之失效,而變成一個右值引用。

引用折疊(Reference collapsing)規則:

  1. 所有的右值引用疊加到右值引用上變成一個右值引用
  2. 所有的其它引用類型疊加都變成一個左值引用
  3. 左值或者右值是獨立於它的類型的,也就是說一個右值引用類型的左值是合法的。
int&& var1 = x; // var1 is of type int&& (no use of auto here)
auto&& var2 = var1; // var2 is of type int& ,var2的類型是universal references(有類型推導)

var1的類型是一個左值類型,但var1本身是一個左值;
var1是一個左值,根據引用折疊規則,var2是一個int&

 

int w1, w2;
auto&& v1 = w1;
decltype(w1)&& v2 = w2; 

v1是一個universal reference,它被一個左值初始化,所以它最終一個左值;
v2是一個右值引用類型,但它被一個左值初始化,一個左值初始化一個右值引用類型是不合法的,所以會編譯報錯。但是如果我希望把一個左值賦給一個右值引用類型該怎么做呢 ,用std::move,decltype(w1)&& v2 = std::move(w2); std::move可以將一個左值轉換成右值,關於std::move將在下一篇博文中介紹。

&&的總結:

  1. 左值和右值是獨立於它們的類型的,一個左值的類型有可能是右值引用類型。
  2. T&&是一個未定的引用類型,它可能是左值引用也可能是右值引用類型,取決於初始化的值類型。
  3. &&成為未定的引用類型的唯一條件是:T&&且發生類型推斷。
  4. 所有的右值引用疊加到右值引用上變成一個右值引用,其它引用折疊都為左值引用。

如果想更詳細了解&&,可以參考scott-meyers這個文章:http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers

右值引用優化性能,避免深拷貝

右值引用是用來支持轉移語義的。轉移語義可以將資源 ( 堆,系統對象等 ) 從一個對象轉移到另一個對象,這樣能夠減少不必要的臨時對象的創建、拷貝以及銷毀,能夠大幅度提高 C++ 應用程序的性能。消除了臨時對象的維護 ( 創建和銷毀 ) 對性能的影響。

以一個簡單的 string 類為示例,實現拷貝構造函數和拷貝賦值操作符。

 class MyString { 
 private: 
  char* m_data; 
  size_t   m_len; 
  void copy_data(const char *s) { 
    m_data = new char[m_len+1]; 
    memcpy(_data, s, m_len); 
    m_data[_len] = '\0'; 
  } 
 public: 
  MyString() { 
    m_data = NULL; 
    m_len = 0; 
  } 

  MyString(const char* p) { 
    m_len = strlen (p); 
    copy_data(p); 
  } 

  MyString(const MyString& str) { 
    m_len = str.m_len; 
    copy_data(str.m_data); 
    std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl; 
  } 

  MyString& operator=(const MyString& str) { 
    if (this != &str) { 
      m_len = str.m_len; 
      copy_data(str._data); 
    } 
    std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl; 
    return *this; 
  } 

  virtual ~MyString() { 
    if (m_data) free(m_data); 
  } 
 }; 

void test() { 
  MyString a; 
  a = MyString("Hello"); 
  std::vector<MyString> vec; 
  vec.push_back(MyString("World")); 
 }

實現了調用拷貝構造函數的操作和拷貝賦值操作符的操作。MyString(“Hello”) 和 MyString(“World”) 都是臨時對象,也就是右值。雖然它們是臨時的,但程序仍然調用了拷貝構造和拷貝賦值,造成了沒有意義的資源申請和釋放的操作。如果能夠直接使用臨時對象已經申請的資源,既能節省資源,有能節省資源申請和釋放的時間。這正是定義轉移語義的目的。

用c++11的右值引用來定義這兩個函數

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; 
 }

有了右值引用和轉移語義,我們在設計和實現類時,對於需要動態申請大量資源的類,應該設計右值引用的拷貝構造函數和賦值函數,以提高應用程序的效率。

c++11 boost技術交流群:296561497,歡迎大家來交流技術。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM