c++11 rvalue reference & perfect forwarding

來源:互聯網
上載者:User
文章目錄
  • std::move
  • function template
  • Reference Collapsing Rule
  • Deduction
  • std::forward
  • Explictly specifiec template parameter
簡介右值引用,是c++11中為瞭解決大對象拷貝效能問題,以及參數傳遞而新加的特性。形如T&&,其中T是referenced type。Lvalue & Rvalue左值和右值在c語言中差不多可以表述為分別出現在運算式左右兩側,但是在c++中,因為引入了class,情況變得更加複雜。基本上可以總結為:
  1. 左值可以運用&操作符取得地址,注意臨時對象是無法取得地址的,因為很容易導致問題。
  2. 左值必然有一個名字
  3. 不是左值的是右值
左值和右值只是運算式的屬性。還有一個概念需要理解就是類型type,類型和左右值不是一個概念,馬上就會講解。我們來看幾個例子,理解一下左右值的概念。
int f();int i;int& g();int&& h();&f; // f is lvalue, &f returns to pointer to the functionf(); // f() is rvalue, as f returns a int by valuei;  // i is lvalueg(); // g() is lvalue, as f returns a lvalue reference to inth(); // h() is rvalue, as f returns a rvalue reference to intvoid f(int&& i){    // i is a rvalue reference, but i is a lvalue as named rvalue reference is lvalue}

這裡需要注意的是函數的傳回值,一個函數調用通常在傳回值是左值引用的時候才是左值(見n3242 5.2.2/10,下面截取標準中的文字以供參考)。

5.2.2/10A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function
type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.
還有一個需要注意的是,一個命名的右值引用是左值,即最後一個給出的情況(見n3242 5/6,下面給出標準中的文字供參考)。
5/6In general, the effect of this rule is that named rvalue references are treated as lvalues and unnamed rvalue
references to objects are treated as xvalues; rvalue references to functions are treated as lvalues whether
named or not
Reference

左值引用形如T&,而右值引用形如T&&,並且我們知道右值引用可以綁定到右值,那麼我們時候可以綁定到一個右值常量?因為常量是不可修改的,但是由於T&&不是reference to const,所以是否成立?

答案是可以的,請看如下例子:

#include <iostream>using namespace std;int main(){    int&& rri = 5;    rri = 4;    cout << rri << endl;    // error    // char const*&& rrcc = "hello";    // *rrcc = '1';}

這裡在g++4.7.3中運行後可以發現rri的值是4。

這裡涉及到reference的初始化,標準中規定,對於使用右值來初始化一個右值引用,可以建立一個拷貝,即,這裡5會被儲存在一個對象中,所以這裡我們對這個對象進行修改。標準中8.5.3/5中如下描述(部分):

8.5.3/5

— Otherwise, the reference shall be an lvalue reference to a non-volatile const type (i.e., cv1 shall be
const), or the reference shall be an rvalue reference.

    If the initializer exrepssion is xvalue or ...

    — Otherwise, a temporary of type “cv1 T1” is created and initialized from the initializer expression
    using the rules for a non-reference copy-initialization

再看下面注釋中的代碼,由於rrcc的類型是reference to pointer to const char,所以我們在後面對char const*賦值時出錯。

Move Constructor我們來看一個move ctor的例子:
class foo{public:    foo(foo&& f)        : m_s(f.m_s) // m_s(move(f.m_s))    {    }private:    string m_s;};

這裡,foo(foo&&)是move ctor,由於f是右值引用,我們認為,我們可以通過直接調用string的move ctor而不做任何處理。這是錯誤的。結果這裡只有string的copy ctor被調用。

原因很簡單,上面我們已經提到,對於一個named rvalue reference,它是一個lvalue,由於rvalue不能綁定lvalue,所以lvalue只能綁定到string的copy ctor。而我們的願意是要move,而不是copy,所以這裡就需要使用標準庫的std::move函數,將左值轉化成右值引用。而對於一個unamed rvalue reference,它是一個右值,這樣就可以調用string的move ctor。std::movestd::move所做的事情很簡單,只是通過static_cast來將一個左值轉換為右值引用,即
static_cast<T&&>(v)

當然具體實現會複雜一下,不過請繼續接著看。Perfect Forwardingperfect forwarding需要解決的問題是爆炸式函數重載。因為我們有了move ctor之後,我們顯然會在函數參數聲明時將參數聲明為T&&,否則如果還是使用T const&來聲明,那麼我們將不能move ctor,這樣就沒有什麼意義了。但是問題在於,使用者可能傳入一個右值,或者一個左值,那麼我們可以重載:

void f(foo const& a);void f(foo&& a);

但是當參數數量變多時怎麼辦,假設有N個參數,那麼顯然我們需要重載2^N個函數才能解決問題,所以引入了perfect forwarding。

function template在使用perfect forwarding時,我們需要結合函數模板和右值引用,即
void g(int const&);void g(int&&);template<typename T>void f(T&& v){    g(forward<T>(v));}

這裡標準庫函數forward完成了類型轉寄。抱枕傳遞給的g的類型的左右值屬性是使用者傳入的屬性。

注意,若且唯若參數為T&&,才會觸發perfect forwarding,引述標準的文字
14.8.2.1/3If P is a cv-qualified type, the top level cv-qualifiers of P’s type are ignored for type deduction. If P is a reference type, the type referred to by P is used for type deduction. If P is an rvalue reference to a cv-unqualified template parameter and
the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.
具體這段話的意思請繼續看。Reference Collapsing Rule我們知道在c++中引用的引用是非法,所以標準中引入如下規則,
  1. T& && = T&
  2. T&& & = T&
  3. T& & = T&
  4. T&& && = T&&

這裡的意思是,當一個類型,比如T& &&時,最終得到的類型是T&。可以看出,僅當T&& &&的情況,類型才是右值引用,其他情況都是左值引用。

Deduction在講解forwarding的原理前,先來瞭解下幾個模板推導中用到的術語,表中中定義P,就是函數模板的參數類型,A是函數模板調用時的使用者給出的類型。推導的目的就是為了匹配P和A,來解析出T。Deduced A就是P經過轉換後的類型(見標準的14.8.2.1),transformed A是當P為特定條件時,變換後得到的類型。回到前面perfect forwarding的講解,先來看函數模板f如何推導template parameter T。下面給出標準中的例子:
Template <class T> int f(T&&);template <class T> int g(const T&&);int i;int n1 = f(i); // calls f<int&>(int&)int n2 = f(0); // calls f<int>(int&&)int n3 = g(i); // error: would call g<int>(const int&&), which               // would bind an rvalue reference to an lvalue

先來看f(i)的調用,根據14.8.2.1/3,我們有

  1. P = T&&,由於P是右值引用,所以Deduced A = T
  2. 由於P是右值引用,並且i是左值,並且i的類型是int,所以Transformed A = int&
  3. Deduced A = Transformed A => T = int&

所以我們將T = int&代入f,得到f<int&>(int& &&),根據reference collapsing rule,我們可以得出f的參數類型是int&,並且是左值,是我們要的。

再來看f(0)的調用,我們有
  1. P = T&&,由於P是右值引用,所以Deduced A = T
  2. 0是右值,所以Transformed A = int
  3. Deduced A = Transformed A => T = int

所以最後得到f的簽名為f<int>(int&&),參數類型是右值引用。

至此保留了使用者參數的左右值屬性。對於最後一個例子,大家可以自己嘗試推導。std::forward這裡給出實現,forward實際有2個重載,但是這裡我們只關心其中一個對左值引用的重載,因為我們知道,具名引數是左值,所以傳遞給forward的必然是左值。
template<typename T>T&& forward(std::remove_reference<T>::type& v){    return static_cast<T&&>(v);}

又是一個函數模板。假設我們指定T = int&,那麼將會有:

  1. remove_reference<T>::type& = int&
  2. static_cast<int& &&>(v) = static_cast<int&>(v)
  3. 返回 int& && = int&

這裡再一次用到了reference collapsing rule,我們將T指定為int&,傳回值也是int&,perfect!!! forwarding。

假設我們指定T = int(這裡我們也可以指定T = int&&,但是通常不這麼寫),有
  1. remove_reference<T>::type& = int&,這裡還是左值引用,沒錯,還記得我們傳遞給forward的也是左值嗎?雖然它是右值引用。
  2. static_cast<int&& &&>(v) = static_cast<int&&>(v)
  3. 返回int&& && = int&&

perfect!!!。

至此,我們完成了完美轉寄,在沒有改變參數類型的情況下,將參數傳遞給了另外一個函數。Explictly specifiec template parameter有的讀者可能注意到了,我們在調用forward時,顯式指定了模板參數T,為什嗎?先來看另一種forward的定義:
template<typename T>T&& forward(T& v){    return static_cast<T&&>(v);}

我們還是可以推匯出,這裡也能夠實現完美轉寄(有的讀者可能認為無法對參數是const的對象進行轉寄,事實不是如此,我們盡在f(T&&)內部使用,所以不存在這個問題,可以試著自己推導)。但是如果一旦不指定T,而是讓模板自動推導,那麼根據前面我們學到的,假設f為:

template<typename T>void f(T&& v){    g(forward(v));}

假設我們傳入一個左值int,那麼v的類型就是int&,而傳入forward的v的類型在進行推導前會被變成int(根據標準中5.5),所以我們推匯出forward的T = int,

最後forward將會返回int&&。出現問題了,左值變成了右值。標準之所以顯式讓我們指定T就是為了防止模板自動推導導致的問題。引用文檔讀者若有興趣,可以參考以下文檔,我從中受益匪淺,這裡寫的也差不多隻是摘錄:
  1. N3242,c++標準草案,有更新的版本,自行google。
  2. C++ Rvalue Reference Explained
  3. ACCU Overload 111 中的 Universal Reference
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.