首頁 >後端開發 >C#.Net教程 >淺析C++11中的右值引用、轉移語意與完美轉發

淺析C++11中的右值引用、轉移語意與完美轉發

高洛峰
高洛峰原創
2017-01-23 14:11:241524瀏覽

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();

的,x的,臨時物件的),相應的在物件被銷毀時也會調用三次析構函數,而如果採用右值引用的方式:

Demo&& x=foo();

   

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

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

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

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

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

         雖然從綁定規則中可以看出常量左值引用也可以綁定到右值,但顯然不可以改變右值的值,右值引用就可以,從而實現轉移語義,因為右值引用通常要改變所綁定的右值,所以被綁定的右值不能為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函数定义在标准库fd21907a0e13328ffda092e1790a5d69中,它的作用是将左值强行转化为右值使用,从实现上讲,std:move等同于static_cast5217b58c2bd313af52a58e21ba9922eb(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_cast5217b58c2bd313af52a58e21ba9922eb ,实际上只在para被推导为右值引用的时候才发挥作用,由于para是左值(右值引用是左值),因此需要将它转为右值后再传入func内,C++ 11在03ebe0a327eab382116018c4d3aba59a定义了一个std::forward8742468051c85b06f0a0af9e3e506b5c函数来实现以上行为,

所以最终版本

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

std::forward的实现与static_cast5217b58c2bd313af52a58e21ba9922eb(para)稍有不同

std::forward函数的用法为forward8742468051c85b06f0a0af9e3e506b5c(para) , 若T为左值引用,para将被转换为T类型的左值,否则para将被转换为T类型右值

总结

以上就是关于C++11中右值引用、转移语义和完美转发的全部内容,这篇文章介绍的很详细,希望对大家的学习工作能有所帮助。

更多浅析C++11中的右值引用、转移语义和完美转发相关文章请关注PHP中文网!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn