談談右值引用

左值與右值

這在之前的一篇博文中有詳細介紹,這裏再簡單說一下。

  • 左值:
    可使用&符號取地址
    可位於賦值操作符=的左側,也可位於右側

  • 右值:
    不能使用&符號取地址
    只能位於賦值操作符的右側

老式的swap函數

template<class T> swap(T& a, T& b) // 老式的swap函數
{
    T tmp(a);
    a = b;
    b = tmp;
}

如果T是一個拷貝代價相當高昂的類型,例如string和vector,那麼上述swap()操作將很耗資源。
標準庫已經針對string和vector的swap()進行了特化來解決這個問題。

新式的swap函數

template <class T>
void swap(T& a, T& b)  // “完美swap”(大多數情況下)
{
      T tmp = std::move(a);
      a = std::move(b);
      b = std::move(tmp);
}

在C++11的標準庫中,所有的容器都提供了移動構造函數和移動賦值操作符,那些插入新元素的操作,如insert()和push_back(), 也都有了可以接受右值引用的版本。

右值引用

右值引用,解決2個問題:

  • 移動語義 (這個比較簡單,本文就不介紹了)
  • 完美轉發 (右值引用解決了完美轉發前一半的問題,後一半是std::forward解決的)

右值引用有3種

  • 不具名右值引用
    static_cast<T&&>(t)
    std::move()

  • 具名右值引用
    可以用&符號取地址,所以具名右值引用是左值。
    其實,具名變量都是左值。
    具名右值引用,讓本應銷燬的純右值又“重獲新生”,使得其生命週期和右值引用類型變量的生命週期一樣長。
    在C++11之前,C++98/03的時候,使用 const 左值引用來使得右值的生命週期延長以進行性能優化,如 const A& a = GetA();

  • 萬能型引用(universal reference)
    包含2種實例:

    1. auto&&類型遍歷
    2. 函數模板的某個參數被聲明爲T&&類型,並且該形參到底是左值還是右值需經過推導才能確定

    這裏的重點是,僅僅是當發生自動類型推導(如函數模板的類型自動推導,或auto關鍵字)的時候,T&&纔是universal references.

template<typename T>
void f(T&& param);      // param是萬能型引用,因爲T&&不是確定的類型

template<typename T>
class Test {
Test(Test&& rhs);       // rhs是普通的右值引用,因爲Test&&已經是確定的類型
};

萬能型引用類型的變量的實際類型由它所綁定的值來確定。如果綁定左值,那該變量就是左值;如果綁定右值,那該變量就是右值。(注:這應該就是C++標準的規定,編譯器廠商實現之。)

對於 auto&& 類型來說,如果所賦之值爲左值,則 auto&& 變量爲左值;如果所賦之值爲右值,則 auto&& 變量爲右值。

class X {};  

// var1是具名右值引用,具名右值引用是左值,所以var1是左值; var1綁定到右值X()
X&& var1 = X();

// var2是萬能型引用,綁定到左值var1;因此,var2是左值,類型推導爲 X&
auto&& var2 = var1;

// var3是萬能型引用,綁定到右值X(); 因此,var3是右值,類型推導爲 X&&
auto&& var3 = X();

對於函數模板的 T&& 的萬能型引用形參,與上述也是類似。

class X{};

template<typename T>  
void g(std::vector<typename T>&& p1);  // p1是右值引用  

template<typename T>  
void f(T&& p2);                        // p2是萬能型引用  
  
X a;    // a是左值
f(a);   // p2綁定左值a,因此 p2 爲左值,其類型推導爲 X&  類型
f(X()); // p2綁定右值, 因此 p2 爲右值,其類型推導爲 X&& 類型

另外,注意 const auto && 和 函數模板參數的 const T&& 類型參數 都是普通右值引用,而不是萬能型引用。

關於萬能型引用,Scott的這篇文章很不錯。

具名右值引用是左值的一個場景

假設有一個類Base,並且通過重載拷貝構造函數和賦值操作符實現了move語義:

Base(Base const & rhs); // non-move semantics
Base(Base&& rhs);       // move semantics

然後有一個繼承自Base的類Derived. 爲了保證Derived對象中的Base部分能夠正確實現move語義,必須也重載Derived類的拷貝構造函數和賦值操作符。
先看左值版本的拷貝構造函數:

Derived(Derived const & rhs)
  : Base(rhs)
{
    // Derived-specific stuff
}

再看錯誤的右值版本的拷貝構造函數:

Derived(Derived&& rhs)
  : Base(rhs) // 錯誤:rhs是個左值
{
    // Derived-specific stuff
}

而正確的右值版本的拷貝構造函數是這樣的:

Derived(Derived&& rhs)
  : Base(std::move(rhs)) // Correct, call Base(Base&& rhs)
{
    // Derived-specific stuff
}

move語義與編譯器優化

有一個下面這樣的函數

X foo()
{
  X x;
  // Do something to x
  // ...
  
  return x; 
}

既然返回的是一個臨時變量,爲了避免“將x拷貝給臨時變量”這個過程,能否將最後一句 return x; 改爲 return std::move(x); 呢?
不能這麼做。因爲現在的編譯器基本上都會做返回值優化(Return Value Optimization),也就是說,對於 return x; 這種操作,編譯器不會去做copy/move構造函數,而是相當於在函數返回的地方直接創建對象,這樣更加優化。反之,如果加了 std::move(x),就達不到C++標準中所說的 Copy Elision,從而仍然存在copy的動作。
關於這個問題,結論與基本解釋就是如上所述,更多細節可參見下面2篇文章:

完美轉發

  • 目的:
    其他函數可以因轉發來的參數的左右值屬性不同而進行不同處理: 參數爲左值時實施拷貝語義;參數爲右值時實施移動語義。

  • 要解決的問題:
    函數模板在向其他函數轉發(即傳遞)該函數模板自身參數(形參)時該如何保留該參數(實參)的左右值屬性。
    即,如果實參是左值,那麼它就應該被轉發爲左值;如果實參是右值,它就應該被轉發爲右值。

  • 實現的難點:
    即便函數模板的參數是萬能型引用,到了函數模板內部,該萬能型引用因爲同時又是具名的,所以仍爲左值;從而使得傳遞給其他函數的時候傳遞的仍是左值。這就無法轉發右值了。
    見下面的程序:

#include<iostream>  
using namespace std;  
  
struct X {};  
void inner(const X&) {cout << "inner(const X&)" << endl;}  
void inner(X&&) {cout << "inner(X&&)" << endl;}  
template<typename T>  
void outer(T&& t) {inner(t);}  
  
int main()  
{  
    X a;  
    outer(a);    // inner(const X&)
    outer(X());  // inner(const X&)
}  

所以,右值引用(其實是萬能型引用),只把完美轉發問題解決了一半,即只能保證形參 T&& 的實際類型可以根據實參來決定是左值引用還是右值引用;另一半問題則是藉助了 std::forward(t) 解決的。

std::forward<T>(t)
標準庫函數 std::forward<T>(t) 有兩個參數:模板參數 T 與 函數參數 t:

  • 當T爲左值引用時,t將被轉換爲無名左值引用
  • 當T爲非引用類型或右值引用時,t將被轉換爲無名右值引用

這裏的重點是,若t是具名右值引用, std::forward<T>(t) 將也是一個右值引用,而非左值。 從而解決了上述的另一半問題。
參考代碼如下:

#include<iostream>  
using namespace std;  
  
struct X {};  
void inner(const X&) {cout << "inner(const X&)" << endl;}  
void inner(X&&) {cout << "inner(X&&)" << endl;}  
template<typename T>  
void outer(T&& t) {inner(forward<T>(t));}  
  
int main()  
{  
    X a;  
    outer(a);    // inner(const X&)
    outer(X());  // inner(X&&)
}  

綜上所述,右值引用主要是用來解決移動語義和完美轉發這2個問題。
關於完美轉發與引用摺疊,後續應該還會有文章談到。本文終。

(完)

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