淺析C++11中的右值引用、轉移語義和完美轉寄

來源:互聯網
上載者:User
1. 左值與右值:

C++對於左值和右值沒有標準定義,但是有一個被廣泛認同的說法:可以取地址的,有名字的,非臨時的就是左值;不能取地址的,沒有名字的,臨時的就是右值.

可見立即數,函數返回的值等都是右值;而非匿名對象(包括變數),函數返回的引用,const對象等都是左值.

從本質上理解,建立和銷毀由編譯器幕後控制的,程式員只能確保在本行代碼有效,就是右值(包括立即數);而使用者建立的,通過範圍規則可知其生存期的,就是左值(包括函數返回的局部變數的引用以及const對象),例如:

int& foo(){int tmp; return tmp;} int fooo(){int tmp; return tmp;} int a=10; const int b; int& temp=foo();//雖然合法,但temp引用了一個已經不存在的對象 int tempp=fooo();

以上代碼中,a,temp和foo()都是非常量左值,b是常量左值,fooo()是非常量右值,10是常量右值,有一點要特別注意:返回的引用是左值(可以取地址)!

一般來說,編譯器是不允許對右值變更的(因為右值的生存期不由程式員掌握,即使更改了右值也未必可以用),對於內建類型對象尤其如此,但C++允許使用右值對象調用成員函數,雖然允許這樣做,但出於同樣原因,最好不要這麼做.

2. 右值引用:

右值引用的表示方法為

Datatype&& variable

右值引用是C++ 11新增的特性,所以C++ 98的引用為左值引用.右值引用用來綁定到右值,綁定到右值以後本來會被銷毀的右值的生存期會延長至與綁定到它的右值引用的生存期,右值引用的存在並不是為了取代左值引用,而是充分利用右值(特別是臨時對象)的建構來減少對象建構和析構操作以達到提高效率的目的,例如對於以下函數:

(Demo是一個類)Demo foo(){  Demo tmp;  return tmp;}

在編譯器不進行RVO(return value optimization)最佳化的前提下以下操作:

Demo x=foo();

將會調用三次建構函式(tmp的,x的,臨時對象的),相應的在對象被銷毀時也會調用三次解構函式,而如果採用右值引用的方式:

Demo&& x=foo();

那麼就不需要進行x的建構,本來本來要被銷毀的臨時對象也會由於x的綁定而將生存期延長至和x一樣(可以理解為x賦予了那個臨時對象一個合法地位:一個名字),就需要提高了效率(代價就是tmp需要佔據4位元組空間,但這是微不足道的).

右值引用與左值引用綁定規則:

常量左值引用可以綁定到常量和非常量左值,常量和非常量右值;

非常量左值引用只能綁定到非常量左值;

非常量右值引用只能綁定到非常量右值(vs2013也可以綁定到常量右值);

常量右值引用只能綁定到常量和非常量右值(非常量右值引用只是為了語義的完整而存在,常量左值引用就可以實現它的作用).

雖然從綁定規則中可以看出常量左值引用也可以綁定到右值,但顯然不可以改變右值的值,右值引用就可以,從而實現轉移語義,因為右值引用通常要改變所綁定的右值,所以被綁定的右值不能為const.

注意:右值引用是左值!

3. 轉移語義(move semantics):

右值引用被引入的目的之一就是實現轉移語義,轉移語義可以將資源 ( 堆,系統對象等 ) 的所有權從一個對象(通常是匿名的臨時對象)轉移到另一個對象,從而減少對象構建及銷毀操作,提高程式效率(這在2的例子中已經作瞭解釋).轉移語義與拷貝語義是相對的.從轉移語義可以看出,實際上,轉移語義並不是新的概念,它實際上已經在C++98/03的語言和庫中被使用了,比如在某些情況下拷貝建構函式的省略(copy constructor elision in some contexts),智能指標的拷貝(auto_ptr “copy”),鏈表拼接(list::splice)和容器內的置換(swap on containers)等,只是還沒有統一的文法和語義支援

雖然普通的函數和操作符也可以利用右值引用實現轉移語義(如2中的例子),但轉移語義通常是通過轉移建構函式和轉移賦值操作符實現的.轉移建構函式的原型為Classname(Typename&&) ,而拷貝建構函式的原型為Classname(const Typename&) ,轉移建構函式不會被編譯器自動產生,需要自己定義,只定義轉移建構函式也不影響編譯器產生拷貝建構函式,如果傳遞的參數是左值,就調用拷貝建構函式,反之,就調用轉移建構函式.

例如:

class Demo{ public:   Demo():p(new int[10000]{};   Demo(Demo&& lre):arr(lre.arr),size(lra.size){lre.arr=NULL;}//轉移建構函式   Demo(const Demo& lre):arr(new int[10000]),size(arr.size){     for(int cou=0;cou<10000;++cou)       arr[cou]=lew.arr[cou];   } private:   int size;   int* arr; }

從以上代碼可以看出,拷貝建構函式在堆中重新開闢了一個大小為10000的int型數組,然後每個元素分別拷貝,而轉移建構函式則是直接接管參數的指標所指向的資源,效率搞下立判!需要注意的是轉移建構函式實參必須是右值,一般是臨時對象,如函數的傳回值等,對於此類臨時對象一般在當行代碼之後就被銷毀,而採用轉移建構函式可以延長其生命期,可謂是物盡其用,同時有避免了重新開闢數組.對於上述代碼中的轉移建構函式,有必要詳細分析一下:

Demo(Demo&& lre):arr(lre.arr),size(lre.size)({lre.arr=NULL;}

lre是一個右值引用,通過它間接訪問實參(臨時對象)的資源來完成資源轉移,lre綁定的對象(必須)是右值,但lre本身是左值;

因為lre是函數的局部對象,”lre.arr=NULL"必不可少,否則函數結尾調用解構函式銷毀lre時仍然會將資源釋放,轉移的資源還是被系統收回.

4. move()函數

3中的例子並非萬能,Demo(Demo&& lre)的實參必須是右值,有時候一個左值即將到達生存期,但是仍然想要使用轉移語義接管它的資源,這時就需要move函數.

std::move函數定義在標準庫<utility>中,它的作用是將左值強行轉化為右值使用,從實現上講,std:move等同於static_cast<T&&>(lvalue) ,由此看出,被轉化的左值本身的生存期和左值屬性並沒有被改變,這類似於const_cast函數.因此被move的實參應該是即將到達生存期的左值,否則的話可能起到反面效果.

5. 完美轉寄(perfect forwarding)

完美轉寄指的是將一組實參"完美"地傳遞給形參,完美指的是參數的const屬性與左右值屬性不變,例如在進行函數封裝的時候,func函數存在下列重載:

void func(const int);void func(int);void func(int&&);

如果要將它們封裝到一個函數cover內,以實現:

void cover(typename para){  func(para);}

使得針對不同實參能在cover內調用相應類型的函數,似乎只能通過對cover進行函數重載,這使代碼變得冗繁,另一種方法就是使用函數模板,但在C++ 11之前,實現該功能的函數模板只能採用值傳遞,如下:

template<typename T>void cover(T para){  ...  func(para);  ...}

但如果傳遞的是一個相當大的對象,又會造成效率問題,要通過引用傳遞實現形參與實參的完美匹配(包裹const屬性與左右值屬性的完美匹配),就要使用C++ 11 新引入的引用摺疊規則:

函數形參 T的類型 推導後的函數形參

T& A& A&
T& A&& A&
T&& A& A&
T&& A&& A&&

因此,對於前例的函數封裝要求,採用以下模板就可以解決:

template<typename T>void cover(T&& para){  ...  func(static_cast<T &&>(para));  ...}

如果傳入的是左值引用,轉寄函數將被執行個體化為:

void func(T& && para){   func(static_cast<T& &&>(para)); }

應用引用摺疊,就為:

void func(T& para){   func(static_cast<T&>(para)); }

如果傳入的是右值引用,轉寄函數將被執行個體化為:

void func(T&& &¶){    func(static_cast<T&& &&>(para));}

應用引用摺疊,就是:

void func(T&& para){   func(static_cast<T&&>(para)); }

對於以上的static_cast<T&&> ,實際上只在para被推導為右值引用的時候才發揮作用,由於para是左值(右值引用是左值),因此需要將它轉為右值後再傳入func內,C++ 11在<untility>定義了一個std::forward<T>函數來實現以上行為,

所以最終版本

template<typename T> void cover(T&& para){   func(forward(forward<T>(para))); }

std::forward的實現與static_cast<T&&>(para)稍有不同

std::forward函數的用法為forward<T>(para) , 若T為左值引用,para將被轉換為T類型的左值,否則para將被轉換為T類型右值

總結

以上就是關於C++11中右值引用、轉移語義和完美轉寄的全部內容,這篇文章介紹的很詳細,希望對大家的學習工作能有所協助。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.