1. 程式人生 > 程式設計 >C++11的右值引用的具體使用

C++11的右值引用的具體使用

C++11 引入了 std::move 語義、右值引用、移動構造和完美轉發這些特性。由於這部分篇幅比較長,分為3篇來進行闡述。

在瞭解這些特性之前,我們先來引入一些問題。

一、問題匯入

  1. 函式返回值是傳值的時候發生幾次物件構造、幾次拷貝?
  2. 函式的形參是值傳遞的時候發生幾次物件構造?

讓我們先來看一段程式碼,

// main.cpp
#include <iostream>
using namespace std;

class A{
public:
  A(){
   cout<<"class A construct!"<<endl;
  } 
  A(const A&){
   cout<<"class A copy!"<<endl;
  }
  A& operator=(const A&){
   cout<<"assignment called!"<<endl;
  }
  ~A(){
   cout<<"class A destruct!"<<endl;
  }
};

A get_A_value(){
  return A();
}
int main(){
  A a = get_A_value();
  return 0;
}

使用 g++ 編譯;注意使用 -fno-elide-constructors關閉省略構造優化

g++ main.cpp -fno-elide-constructors

可以得到以下輸出

class A construct!
class A destruct!
class A copy!
class A destruct!
class A destruct!

可以看到A a=get_A_value(); 一行程式碼居然產生1次物件構造和2次物件的拷貝構造!具體為

  • 在 get_A_value() 裡 A() 構造了臨時物件,發生了一次構造;
  • 函式返回的時候會把臨時物件拷貝後作為返回值,發生一次拷貝;
  • A a = 函式返回值發生了拷貝構造。

如果使用編譯器優化(預設),則會把臨時物件拷貝的那次 和 用返回值構造最終物件的拷貝的給省略了;也即,只有一次拷貝和析構。

class A construct!
class A destruct!

如果把上面程式碼改一下

// ... A

void pass_A_by_value(A a){

}
int main(){
  A a;
  pass_A_by_value(a);
  return 0;
}

在去掉優化 g++ main.cpp -fno-elide-constructors時輸出為

class A construct!

class A copy!
class A destruct!
class A destruct!

1次構造加上1次拷貝。

因此,下次如果面試的時候有人問這個問題,你就可以說:預設情況下經過編譯器優化後臨時物件的拷貝就會被省去,如果使用 -fno-elide-constructors 省略優化,則還要考慮臨時物件的拷貝。

事實上,在未經優化的情況下,以下時候拷貝建構函式會被呼叫:

  1. 函式內的區域性物件做為返回值返回(不是引用)的時候會發生拷貝(拷貝為臨時物件返回)
  2. 函式形參為傳值的時候,會發生拷貝構造
  3. 一個物件以另外一個物件進行初始化的時候

物件的頻繁構造是程式的開銷,特別是當物件內部有堆上記憶體(比如有 new 出來的成員)的時候,每次拷貝構造的時候都需要用 new 申請一塊記憶體,造成效能的降低。對於情況2,好習慣是如果函式引數是隻讀的(也即不會在程式內進行修改),傳引用作為引數,也即 pass_A_by_refrence(const A &a); 對於情況1,編譯器會為我們進行優化; 對於情況3,C++11 引入了一種移動建構函式的概念,它將獲取**右值引用*,右值的“資源” move 到新物件中,這個過程中不會申請新的記憶體,從而達到提高了效率和效能。

所以,要理解些關鍵詞 “移動構造”、“移動語義” ,首先要理解右值和右值引用。

二、右值和右值引用

2.1 左值(lvalue)和右值(rvalue)

在 一、問題匯入 裡我們提到了臨時物件,也即函式返回值的時候只會“臨時”存在的物件(執行超過那一行就會結束它的生存期),這個臨時返回值就是一個右值;
右值的最直觀的定義為,顧名思義:

位於賦值運算子 = 右邊的值,為右值;在左邊的則為左值

A a = foo(); // foo() 為右值
char *x = "thu"; // “thu”為字面值也為右值
a = b + c; // b + c這個結果也是一個右值

在C++中,還有個定義為:

左值可以取得地址、有名字; 不可以取得地址、沒有名字的為右值。

所以 A a = foo()可以用 &a取得a的地址,a 是左值,然是不能取得 foo()的地址,(&foo())無法通過編譯, foo()返回的臨時物件也是沒有名字的,所以是右值。

在C++11中,右值包括兩種,一中是將亡值(xvalue,eXpiring Value),一種是純右值(prvalue,Pure Rvalue)[1]。函式非引用返回的臨時物件、運算表示式的結果、1,3.14,'c'這樣的字面值等都屬於純右值。而xvalue則是由 C++11引入的 如返回值為 A&& 的函式返回值或者std::move()的返回值等。

不深究的話,我們只需要知道左值和右值的區別就行了。對於右值的詳細分類則不必深究。

2.2 左值引用和右值引用

左值引用就是一般的引用,一般用一個&表示,例如

const A &a_ref = a; // 取得物件 a 的引用

左值引用相當於別名,指向一個具體的物件。

右值引用

右值引用顧名思義,就是右值的引用, 用 &&表示;
A &&r_ref = getRvalue(); // r_ref 是一個右值引用
右值引用也相當於別名,與左值的區別為右值引用是無名變數的別名。

getRvalue() 是一個返回右值的函式,右值在這一句執行完就該結束他的生存期了,如果是物件就該呼叫析構函數了;但是==右值引用讓它強行續命==;使用右值引用指向右值,右值的生存期和右值引用一樣長了,這也就少一次物件的析構和構造了。

C++的右值引用主要有兩個用處,一個是移動語義,一個是完美轉發。這個將在接下來的兩篇來講。

總結

為了匯入右值和移動語義,首先複習了以下臨時物件在函式返回值和傳引數時構造了幾次;然後對比介紹了左值和右值,以及右值引用的形式和含義。為移動語義和完美轉發的介紹做鋪墊。

參考資料

Michale Wang| IBM XL 編譯器中國 《深入理解C++11》,機械工業出版社

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。