C++ 新特性 筆記 2 右值引用

C ++ Rvalue引用說明

如下內容,主要是上述連接的摘要html

介紹

Rvalue引用是C ++的一個特性,它是隨C ++ 11標準添加的。使右值參考有點難以理解的是,當你第一次看到它們時,不清楚它們的目的是什麼或它們解決了什麼問題。所以,我不會直接進入並解釋rvalue引用是什麼。相反,我將從要解決的問題開始,而後展現右值引用如何提供解決方案。這樣,右值參考的定義對您來講彷佛是合理和天然的。
Rvalue引用解決了至少兩個問題:c++

&embp * 實現移動語義
&embp * 完美轉發算法

從C的最先期開始的左值和右值的原始定義以下:左值是能夠出如今賦值的左側或右側的表達式,而右值是隻能出如今賦值表達式的右側。
在C ++中,這仍然是左值和右值的第一個直觀方法。可是,C ++及其用戶定義類型引入了一些關於可修改性和可賦值性的細微之處,致使該定義不正確。咱們沒有必要進一步研究這個問題。這是一個替代定義,雖然它仍然能夠與之爭論,但它將使你可以處理rvalue引用:lvalue是一個表達式,它引用一個內存位置並容許咱們經過如下方式獲取該內存位置的地址:& operator 。右值表達式不是左值。ide

// lvalues:
  //
  int i = 42;
  i = 43; // ok, i is an lvalue
  int* p = &i; // ok, i is an lvalue
  int& foo();
  foo() = 42; // ok, foo() is an lvalue
  int* p1 = &foo(); // ok, foo() is an lvalue

  // rvalues:
  //
  int foobar();
  int j = 0;
  j = foobar(); // ok, foobar() is an rvalue
  int* p2 = &foobar(); // error, cannot take the address of an rvalue
  j = 42; // ok, 42 is an rvalue

移動語義

假設X是一個包含某個資源的指針或句柄m_pResource的類。邏輯上,X的複製賦值運算符 以下所示:函數

X& X::operator=(X const & rhs)
{
  // [...]
  // Make a clone of what rhs.m_pResource refers to.  clone 指向的資源,
  // Destruct the resource that m_pResource refers to.  銷燬指向的資源
  // Attach the clone to m_pResource. 將clone 來的資源指向m_pResource
  // [...]
}

相似的推理適用於複製構造函數。如今假設X使用以下:性能

X foo();
X x;
// perhaps use x in various ways
x = foo();

x = foo() 執行了 1) clone foo返回的臨時資源, 2) 銷燬x自身的資源並用clone的資源替換 3) 銷燬臨時資源。
顯然,交換x和臨時對象的資源指針(句柄)會更好,也更有效率,而後讓臨時的析構函數破壞x原始資源。換句話說,在 = 的右側是右值的特殊狀況下,咱們但願複製賦值運算符的行爲以下:優化

// [...] 
//交換m_pResource和rhs.m_pResource 
// [...]

這稱爲移動語義。使用C ++ 11,能夠經過重載實現此條件行爲:this

X& X::operator=(<mystery type> rhs)
{
  // [...]
  // swap this->m_pResource and rhs.m_pResource
  // [...]  
}

右值引用

若是X是任何類型的,則X&&稱爲右值引用到X。爲了更好地區分,普通引用X&如今也稱爲左值引用。
右值引用是一種與普通引用很是類似的類型,但X&有一些例外。最重要的一點是,當涉及函數重載解析時,左值更傾向舊式左值引用,而右值更傾向於新的右值引用:編碼

void foo(X&x); //左值引用 overload
void foo(X && x); // 右值引用 overload 

X x; 
X foobar(); 

FOO(x); //參數是左值:調用foo(X&)
foo( foobar() ); //參數是rvalue:調用foo(X&&)

重點是:.net

rhd 引用容許函數在編譯時branch (經過重載解析), 條件是 "我是在 lvalue 上被調用仍是在 rvalue 上被調用?"
確實,能夠以這種方式重載任何函數,如上所示。可是在絕大多數狀況下,爲了實現移動語義,只有複製構造函數和賦值運算符纔會出現這種重載:

X& X::operator=(X const & rhs); // classical implementation
X& X::operator=(X&& rhs)
{
  // Move semantics: exchange content between this and rhs
  return *this;
}

爲複製構造函數實現rvalue引用重載是相似的。

警告: 正如在C ++中常常發生的那樣,乍一看看起來恰到好處仍然有點完美。事實證實,在某些狀況下,上面的複製賦值運算符之間this和之間的簡單內容交換rhs還不夠好。咱們將在下面的第4節「強制移動語義」中再次討論這個問題。

強制移動語義

C ++ 11容許您不只在rvalues上使用移動語義,並且還能夠自行決定使用左值。一個很好的例子是std庫函數swap。和之前同樣,讓X成爲一個類,咱們已經重載了複製構造函數和複製賦值運算符,以實現對rvalues的移動語義。

template<class T>
void swap(T& a, T& b) 
{ 
  T tmp(a);
  a = b; 
  b = tmp; 
} 

X a, b;
swap(a, b);

這裏沒有rvalues。所以 swap函數的三行並無使用 移動語義。但咱們知道移動語義是能夠的: 不管變量出如今何處, 該變量都是做爲副本構造或賦值的源出現的, 該變量要麼根本不被使用, 要麼僅用做賦值的目標。在C ++ 11中,有一個std庫函數被稱爲std::move,來拯救咱們。它是一個函數,能夠將其參數轉換爲右值,而無需執行任何其餘操做。所以,在C ++ 11中,std庫函數swap 以下所示:

template<class T> 
void swap(T& a, T& b) 
{ 
  T tmp(std::move(a));
  a = std::move(b); 
  b = std::move(tmp);
} 

X a, b;
swap(a, b);

如今全部swap函數的三行都會移動語義。請注意,對於那些沒有實現移動語義的類型(也就是說,不要使用rvalue引用版本重載它們的複製構造函數和賦值運算符),新swap行爲就像舊的行爲同樣。
如上所述,std::move咱們能夠在任何地方 使用swap,爲咱們帶來如下重要好處:

  • 對於那些實現移動語義的類型,許多標準算法和操做將使用移動語義,所以可能會得到潛在的顯着性能提高。一個重要的例子是就地排序:就地排序算法除了交換元素以外幾乎沒有其餘任何東西,這種交換如今將利用全部提供它的類型的移動語義。
  • STL一般須要某些類型的可複製性,例如,可用做容器元素的類型。仔細檢查後發現,在許多狀況下,可移動性就足夠了。所以,咱們如今可使用可移動但不可複製的類型(unique_pointer想到)在許多之前不容許使用的地方。例如,這些類型如今能夠用做STL容器元素。

如今咱們知道了std::move,咱們能夠看到爲何我以前展現的複製賦值運算符的rvalue引用重載的 實現仍然有點問題。考慮變量之間的簡單分配,以下所示:
a = b;
你指望在這裏發生什麼?你但願將所持有的對象a替換爲副本b,而且在替換過程當中,之前對象a保留的資源被破壞。如今考慮一下下邊這句:
a = std :: move(b);
若是移動語義做爲一個簡單的交換來實現,那麼這樣作的效果是,經過持有的對象 a和b正在之間交換a和b。什麼都沒有被破壞。當b不在函數範圍內時,之前由a所持有的對象固然最終會被破壞。固然,除非b再次被move,不然之前a所持有的對象就會被銷燬。所以,就複製賦值算子的實現者而言,不知道之前保持的對象a什麼時候被破壞。
因此從某種意義上說,咱們已經在這裏進入了非肯定性毀滅的暗界:一個變量被分配給了(新內容),但以前由該變量持有的對象仍然存在於某個地方。只要對該物體的破壞沒有外界可見的任何反作用, 這就能夠了。但有時候析構函數會產生這樣的反作用。一個例子是在析構函數中釋放一個鎖。所以, 對象銷燬中具備反作用的任何部分都應在拷貝構造運算符的 rvalue 引用重載中顯式執行:

X& X::operator=(X&& rhs)
{

  // Perform a cleanup that takes care of at least those parts of the
  // destructor that have side effects. Be sure to leave the object
  // in a destructible and assignable state.

  // Move semantics: exchange content between this and rhs
  
  return *this;
}

右值引用是右值嗎?

和之前同樣,讓X成爲一個類,咱們已經重載了複製構造函數和複製賦值運算符來實現移動語義。如今考慮:

void foo(X&& x)
{
  X anotherX = x;
  // ...
}

問題是:在foo函數中,X調用的是哪一個拷貝構造運算符的重載呢?這裏 x 被聲明爲右值引用,一般,一個引用最好是右值(儘管也不是必須的)。所以指望x和一個右值綁定也是合理的,即: X(X&& rhs); 應該被調用。換句話說,人們可能指望任何被聲明爲右值引用的東西自己就是一個右值。右值引用的設計者選擇了一個比這更微妙的解決方案:

聲明爲右值參考的 things 能夠是左值或右值。區別標準是:若是它有一個名字,那麼它就是一個左值。不然,它是一個右值。

在上面的例子中,聲明爲右值引用的東西有一個名稱,所以它是一個左值:

void foo(X&& x)
{
  X anotherX = x; // calls  **X(X const & rhs)**
}

下邊是一個被聲明爲右值引用但沒有名稱的例子,所以是一個右值:

X&& goo();    //對於我而言,須要注意。老是混淆:本行,本質是一個 goo() 函數返回的 X&& 的無名引用
X x = goo(); // calls **X(X&& rhs)** because the thing on
             // the right hand side has no name

如下是設計背後的基本原理:若是容許將移動語義默認應用於具備名稱的內容,如

X anotherX = x;
  // x is still in scope!

中, 會形成危險的混亂和容易出錯, 由於咱們剛剛移動的東西, 即咱們剛剛盜竊的東西, 仍然能夠在後續的代碼行中訪問。但移動語義的所有意義在於只在 "不重要" 的地方應用它, 也就是說, 咱們移動的東西在移動後就會死亡並消失。所以有規則,「若是它有一個名字,那麼它是一個左值。」

那麼另外一部分呢,「若是它沒有名字,那麼它是一個右值?」 查看上面goo的示例,從技術上講,示例第二行中的表達式 goo()所引用的內容在移動以後仍可訪問,這是可能的,儘管不太可能。但回想一下上一節:有時候這就是咱們想要的!咱們但願可以根據本身的判斷強制在左值上移動語義,而且正是規則「若是它沒有名稱,那麼它是一個rvalue」容許咱們以受控的方式實現它。這就是函數的std::move 工做原理。儘管如今向您展現確切的實施還爲時尚早,但咱們距離理解std::move還有一步之遙。它經過引用直接傳遞它的參數,根本不執行任何操做,其結果類型是右值引用。因此表達式std::move(x) 被聲明爲一個右值引用,沒有名稱。所以,它是一個右值,所以, std::move "將其參數轉換爲 rvalue, 即便它不是,", 它經過 "隱藏名稱" 來實現這一點。

假設您已經編寫了一個類Base,而且您已經經過重載Base的複製構造函數和賦值運算符實現了移動語義:

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

你編寫一個Derived派生自的類Base。爲了確保將移動語義應用於對象的Base一部分,您Derived還必須重載Derived複製構造函數和賦值運算符。咱們來看看複製構造函數。相似地處理複製賦值運算符。左值的版本很簡單:

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

rvalues的版本有一個很大的微妙。如下是不瞭解if-it-a-name規則的人 可能作過的事情:

Derived(Derived&& rhs) 
  : Base(rhs) // wrong: rhs is an lvalue
{
  // Derived-specific stuff
}

若是咱們這樣編碼,Base那麼將調用非移動版本的複製構造函數,由於rhs具備名稱的是左值。咱們想要被稱爲Base移動複製構造函數,得到它的方法是編寫

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

移動語義和編譯器優化

考慮如下函數定義:

X foo()
{
  X x;
  // perhaps do something to x
  return x;
}

如今假設像之前同樣,X是一個類,咱們已經重載了複製構造函數和複製賦值運算符來實現移動語義。若是你將上面的函數用於定義一個值,你可能會想說,等一下,這裏有一個值複製,從x到foo返回值的位置。讓我確保咱們使用移動語義代替:

```cpp
X foo()
{
X x;
// perhaps do something to x
return std::move(x); // making it worse!
}
···

不幸的是,這會讓事情變得更糟而不是更好。任何現代編譯器都會將 返回值優化應用於原始函數定義。換句話說,x不是編譯器在本地構造而後將其複製出來,而是直接在foo返回值的位置構造對象。顯然,這甚至比移動語義更好。

完美的轉發:問題

除了 rvalue 引用設計要解決的移動語義以外, 另外一個問題是完美的轉發問題。請考慮如下簡單的工廠功能:

template<typename T, typename Arg> 
shared_ptr<T> factory(Arg arg)
{ 
  return shared_ptr<T>(new T(arg));
}

顯然, 這裏的目的是將參數 arg 從工廠函數轉發到 t 的構造函數。理想狀況下, 就 arg 而言, 一切都應該表現得就像工廠函數不存在, 構造函數直接在客戶端代碼中調用: 完美轉發。上面的代碼在這一點上失敗得很慘: 它引入了一個額外的按值的調用, 若是構造函數經過引用獲取它的參數,這個(按值的調用)特別糟糕。
最多見的解決方案,例如boost::bind,經過引用讓外部函數接受參數:

emplate<typename T, typename Arg> 
shared_ptr<T> factory(Arg& arg)
{ 
  return shared_ptr<T>(new T(arg));
}

這樣更好,但並不完美。問題是如今,沒法在rvalues上調用工廠函數:

factory<X>(hoo()); // error if hoo returns by value
factory<X>(41); // error

這能夠經過提供一個重載來修復,該重載經過const引用獲取其參數:

template<typename T, typename Arg> 
shared_ptr<T> factory(Arg const & arg)
{ 
  return shared_ptr<T>(new T(arg));
}

這種方法存在兩個問題。首先,若是factory不是一個,但有幾個參數,則必須爲各類參數的非const和const引用的全部組合提供重載。所以,該解決方案對具備多個參數的函數的擴展性極差。其次, 這種轉發不是十全十美的, 由於它阻止了移動語義: 工廠主體中 T 構造函數的參數是一個值。所以, 即便沒有包裝函數, 也永遠不會發生移動語義。
事實證實,右值引用可用於解決這兩個問題。它們能夠在不使用重載的狀況下實現真正完美的轉發。爲了理解如何,咱們須要再考慮另外兩個rvalue引用規則。

完美的轉發:解決方案

rvalue 引用的其他兩個規則中的第一個規則也會影響舊式的 lvalue 引用。回想一下, 在 11 c++ 以前, 不容許引用引用: 相似 a & & 會致使編譯錯誤。相比之下, C++11 引入瞭如下引用摺疊規則:

  • A& & becomes A&
  • A& && becomes A&
  • A&& & becomes A&
  • A&& && becomes A&&

其次,函數模板有一個特殊的模板參數推導規則,它經過對模板參數的rvalue引用來獲取參數:

template<typename T>
void foo(T&&);

在這裏,如下適用:
*當 foo 在 A 型的 lvalue 上被調用時, T 解析爲 A &, 所以, 經過上面的引用摺疊規則, 參數類型有效地成爲A&

  • 當在 A 型的 rvalue 上調用 foo 時,T解析爲 A, 所以參數類型變爲 A&&
    根據這些規則,咱們如今可使用rvalue引用來解決上一節中提出的完美轉發問題。這是解決方案的樣子:
template<typename T, typename Arg> 
shared_ptr<T> factory(Arg&& arg)
{ 
  return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}

std::forward 定義以下:

template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
  return static_cast<S&&>(a);
}

爲了瞭解上面的代碼是如何實現完美轉發的, 咱們將分別討論當咱們的工廠函數在 lvalues 和 rvalues 上被調用時會發生什麼。讓 A 和 X 做爲類型。假設首先在 類型位爲X的 lvalue 上調用factory<A>

X x;
factory<A>(x);

而後,經過上面提到的特殊模板推導規則,將factory模板參數Arg解析爲X&。所以,編譯器將建立如下實例:factorystd::forward

shared_ptr<A> factory(X& && arg)
{ 
  return shared_ptr<A>(new A(std::forward<X&>(arg)));
} 

X& && forward(remove_reference<X&>::type& a) noexcept
{
  return static_cast<X& &&>(a);
}

在評估remove_reference並應用參考摺疊規則後,這將變爲:

shared_ptr<A> factory(X& arg)
{ 
  return shared_ptr<A>(new A(std::forward<X&>(arg)));
} 

X& std::forward(X& a) 
{
  return static_cast<X&>(a);
}

這確定是左值的完美轉發:arg工廠函數的參數A經過兩個間接級別傳遞給構造函數,二者都是經過老式的左值引用。

接下來,假設在類型爲X的右值調用factory<A>

X foo();
factory<A>(foo());

而後,再次經過上面提到的特殊模板推導規則,將factory模板參數Arg解析爲X。所以,編譯器如今將建立如下函數模板實例化:

shared_ptr<A> factory(X&& arg)
{ 
  return shared_ptr<A>(new A(std::forward<X>(arg)));
} 

X&& forward(X& a) noexcept
{
  return static_cast<X&&>(a);
}

這確實是rvalues的完美轉發:工廠函數的參數A經過引用的兩個間接層傳遞給構造函數。此外,A的構造函數將其聲明爲一個表達式,該表達式被聲明爲右值引用,而且沒有名稱。根據 無名稱規則,這樣的事情是一個右值。所以, A在rvalue上調用構造函數。這意味着轉發會保留了工廠包裝器不存在時可能發生的任何移動語義。
值得注意的是,保留移動語義其實是std :: forward在這種狀況下的惟一目的。若是不使用std :: forward,一切都會很好地工做,一切都會很好地工做, 只是 a 的構造函數老是會將具備名稱的東西視爲其參數, 而這樣的東西就是一個 lvalue。另外一種說法就是說std​​ :: forward的目的是轉發信息,不管是在調用點包裝器看到左值,仍是右值。
爲何須要std :: forward定義中的remove_reference? 答案是,根本不須要它。若是您在std :: forward的定義中只使用S&而不是remove_reference <S> :: type&,您能夠重複上面的案例區分寫區分來講服本身完美轉發仍然能夠正常工做。可是,只要咱們明確指定Arg做爲std :: forward的模板參數,它就能夠正常工做。 std :: forward定義中remove_reference的目的是強迫咱們這樣作。
咱們快作完了。只剩下研究std:: move的實現狀況了。請記住, std:: move的目的是經過引用直接傳遞它的參數, 並使其像 rvalue 同樣綁定。下面是實現:

template<class T> 
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
  typedef typename remove_reference<T>::type&& RvalRef;
  return static_cast<RvalRef>(a);
}

假設咱們調用std::move了一個類型的左值X:

X x;
std::move(x);

經過新的特殊模板推導規則,模板參數T將解析爲X&。所以,編譯器最終實例化的是:

typename remove_reference<X&>::type&&
std::move(X& && a) noexcept
{
  typedef typename remove_reference<X&>::type&& RvalRef;
  return static_cast<RvalRef>(a);
}

在評估remove_reference並應用新的參考摺疊規則後,這就變成了:

X&& std::move(X& a) noexcept
{
  return static_cast<X&&>(a);
}

這樣作:咱們的左值x將綁定到做爲參數類型的左值引用,函數將其直接傳遞,將其轉換爲未命名的右值引用。
我留給你說服本身std::move在rvalue上調用時實際上工做正常。可是你可能想要跳過這個:爲何有人想要調用std::move 右值,當它的惟一目的是將事物變成右值?此外,你如今可能已經注意到了,而不是:

std::move(x);

你也能夠寫 static_cast<X&&>(x); 然而,std::move強烈優選,由於它更具表現力。

相關文章
相關標籤/搜索