溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

c++11新標(biāo)準(zhǔn)中移動(dòng)語(yǔ)義與右值引用是什么

發(fā)布時(shí)間:2020-08-13 11:59:00 來(lái)源:億速云 閱讀:223 作者:小新 欄目:開(kāi)發(fā)技術(shù)

這篇文章主要介紹c++11新標(biāo)準(zhǔn)中移動(dòng)語(yǔ)義與右值引用是什么,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!

1.移動(dòng)語(yǔ)義

C++11新標(biāo)準(zhǔn)中一個(gè)最主要的特性就是提供了移動(dòng)而非拷貝對(duì)象的能力。如此做的好處就是,在某些情況下,對(duì)象拷貝后就立即被銷(xiāo)毀了,此時(shí)如果移動(dòng)而非拷貝對(duì)象會(huì)大幅提升性能。參考如下程序:

//moveobj.cpp

#include <iostream>
#include <vector>
using namespace std;

class Obj
{
public:
 Obj(){cout <<"create obj" << endl;}
 Obj(const Obj& other){cout<<"copy create obj"<<endl;}
};

vector<Obj> foo() 
{
 vector<Obj> c;
   c.push_back(Obj());
 cout<<"---- exit foo ----"<<endl;
   return c;
}

int main()
{
 vector<Obj> v;
 v=foo();
}

編譯并運(yùn)行:

[b3335@localhost test]$ g++ moveobj.cpp
[b3335@localhost test]$ ./a.out
create obj
copy create obj
---- exit foo ----
copy create obj

可見(jiàn),對(duì)obj對(duì)象執(zhí)行了兩次拷貝構(gòu)造。vector是一個(gè)常用的容器了,我們可以很容易的分析這這兩次拷貝構(gòu)造的時(shí)機(jī):
 (1)第一次是在函數(shù)foo中通過(guò)臨時(shí)Obj的對(duì)象Obj()構(gòu)造一個(gè)Obj對(duì)象并入vector中;
 (2)第二次是通過(guò)從函數(shù)foo中返回的臨時(shí)的vector對(duì)象來(lái)給v賦值時(shí)發(fā)生了元素的拷貝。

由于對(duì)象的拷貝構(gòu)造的開(kāi)銷(xiāo)是非常大的,因此我們想就可能避免他們。其中,第一次拷貝構(gòu)造是vector的特性所決定的,不可避免。但第二次拷貝構(gòu)造,在C++ 11中就是可以避免的了。

[b3335@localhost test]$ g++ -std=c++11 moveobj.cpp
[b3335@localhost test]$ ./a.out
create obj
copy create obj
---- exit foo ----

可以看到,我們除了加上了一個(gè)-std=c++11選項(xiàng)外,什么都沒(méi)干,但現(xiàn)在就把第二次的拷貝構(gòu)造給去掉了。它是如何實(shí)現(xiàn)這一過(guò)程的呢?

在老版本中,當(dāng)我們執(zhí)行第二行的賦值操作的時(shí)候,執(zhí)行過(guò)程如下:

 (1)foo()函數(shù)返回一個(gè)臨時(shí)對(duì)象(這里用tmp來(lái)標(biāo)識(shí)它);
 (2)執(zhí)行vector的 ‘=' 函數(shù),將對(duì)象v中的現(xiàn)有成員刪除,將tmp的成員復(fù)制到v中來(lái);
 (3)刪除臨時(shí)對(duì)象tmp。

在C++11的版本中,執(zhí)行過(guò)程如下:

 (1)foo()函數(shù)返回一個(gè)臨時(shí)對(duì)象(這里用tmp來(lái)標(biāo)識(shí)它);
 (2)執(zhí)行vector的 ‘=' 函數(shù),釋放對(duì)象v中的成員,并將tmp的成員移動(dòng)到v中,此時(shí)v中的成員就被替換成了tmp中的成員;
 (3)刪除臨時(shí)對(duì)象tmp。

關(guān)鍵的過(guò)程就是第2步,它是移動(dòng)而不是復(fù)制,從而避免了成員的拷貝,但效果卻是一樣的。不用修改代碼,性能卻得到了提升,對(duì)于程序員來(lái)說(shuō)就是一份免費(fèi)的午餐。但是,這份免費(fèi)的午餐也不是無(wú)條件就可以獲取的,需要帶上-std=c++11來(lái)編譯。

2.右值引用

2.1右值引用簡(jiǎn)介

為了支持移動(dòng)操作,C++11引入了一種新的引用類型——右值引用(rvalue reference)。所謂的右值引用指的是必須綁定到右值的引用。使用&&來(lái)獲取右值引用。這里給右值下個(gè)定義:只能出現(xiàn)在賦值運(yùn)算符右邊的表達(dá)式才是右值。相應(yīng)的,能夠出現(xiàn)在賦值運(yùn)算符左邊的表達(dá)式就是左值,注意,左值也可以出現(xiàn)在賦值運(yùn)算符的右邊。對(duì)于常規(guī)引用,為了與右值引用區(qū)別開(kāi)來(lái),我們可以稱之為左值引用(lvalue reference)。下面是左值引用與右值引用示例:

int i=42;
int& r=i;  //正確,左值引用
int&& rr=i;  //錯(cuò)誤,不能將右值引用綁定到一個(gè)左值上
int& r2=i*42; //錯(cuò)誤,i*42是一個(gè)右值
const int& r3=i*42; //正確:可以將一個(gè)const的引用綁定到一個(gè)右值上
int&& rr2=i*42; //正確:將rr2綁定到乘法結(jié)果上

從上面可以看到左值與右值的區(qū)別有:
 (1)左值一般是可尋址的變量,右值一般是不可尋址的字面常量或者是在表達(dá)式求值過(guò)程中創(chuàng)建的可尋址的無(wú)名臨時(shí)對(duì)象;
 (2)左值具有持久性,右值具有短暫性。

不可尋址的字面常量一般會(huì)事先生成一個(gè)無(wú)名臨時(shí)對(duì)象,再對(duì)其建立右值引用。所以右值引用一般綁定到無(wú)名臨時(shí)對(duì)象,無(wú)名臨時(shí)對(duì)象具有如下兩個(gè)特性:

 (1)臨時(shí)對(duì)象將要被銷(xiāo)毀;
 (2)臨時(shí)對(duì)象無(wú)其他用戶。

 這兩個(gè)特性意味著,使用右值引用的代碼可以自由地接管所引用的對(duì)象的資源。

2.2 std::move 強(qiáng)制轉(zhuǎn)化為右值引用

雖然不能直接對(duì)左值建立右值引用,但是我們可以顯示地將一個(gè)左值轉(zhuǎn)換為對(duì)應(yīng)的右值引用類型。我們可以通過(guò)調(diào)用C++11在標(biāo)準(zhǔn)庫(kù)中<utility>中提供的模板函數(shù)std::move來(lái)獲得綁定到左值的右值引用。示例如下:

int&& rr1=42;
int&& rr2=rr1;				//error,表達(dá)式rr1是左值
int&& rr2=std::move(rr1);	//ok

上面的代碼說(shuō)明了右值引用也是左值,不能對(duì)右值引用建立右值引用。move告訴編譯器,在對(duì)一個(gè)左值建立右值引用后,除了對(duì)左值進(jìn)行銷(xiāo)毀和重新賦值,不能夠再訪問(wèn)它。std::move在VC10.0版本的STL庫(kù)中定義如下:

/*
 * @brief Convert a value to an rvalue.
 * @param __t A thing of arbitrary type.
 * @return The parameter cast to an rvalue-reference to allow moving it.
*/
template<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept{
	return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}

template<class _Ty> struct remove_reference{ 
  // remove reference
  typedef _Ty type;
};

template<class _Ty> struct remove_reference<_Ty&>{  
  // remove reference
  typedef _Ty type;
};

template<class _Ty> struct remove_reference<_Ty&&>{    
  // remove rvalue reference
  typedef _Ty type;
};

move的參數(shù)是接收一個(gè)任意類型的右值引用,通過(guò)引用折疊,此參數(shù)可以與任意類型實(shí)參匹配。特別的,我們既可以傳遞左值,也可以傳遞右值給std::move:

string s1("hi");
string&& s2=std::move(string("bye")); //正確:從一個(gè)右值移動(dòng)數(shù)據(jù) 
string&& s3=std::move(s1);  //正確:在賦值之后,s1的值是不確定的

注意:
 (1)std::move函數(shù)名稱具有一定迷惑性,實(shí)際上std::move并沒(méi)有移動(dòng)任何東西,本質(zhì)上就是一個(gè)static_cast<T&&>,它唯一的功能是將一個(gè)左值強(qiáng)制轉(zhuǎn)化為右值引用,進(jìn)而可以使用右值引用使用該值,以用于移動(dòng)語(yǔ)義。

(2)typename為什么會(huì)出現(xiàn)在std::move返回值前面?這里需要明白typename的兩個(gè)作用,一個(gè)是申明模板中的類型參數(shù),二是在模板中標(biāo)明“內(nèi)嵌依賴類型名”(nested dependent type name)[3]^{[3]}[3]?!皟?nèi)嵌依賴類型名”中“內(nèi)嵌”是指類型定義在類中。以上type是定義在struct remove_reference;“依賴”是指依賴于一個(gè)模板參數(shù),上面的std::remove_reference<_Tp>::type&&依賴模板參數(shù)_Tp?!邦愋兔笔侵高@里最終要使用的是個(gè)類型名,而不是變量。

2.3 std::forward實(shí)現(xiàn)完美轉(zhuǎn)發(fā)

完美轉(zhuǎn)發(fā)(perfect forwarding)指在函數(shù)模板中,完全依照模板參數(shù)的類型,將參數(shù)傳遞給函數(shù)模板中調(diào)用的另外一個(gè)函數(shù),如:

template<typename T> void IamForwording(T t)
{
 IrunCodeActually(t);
}

其中,IamForwording是一個(gè)轉(zhuǎn)發(fā)函數(shù)模板,函數(shù)IrunCodeActually則是真正執(zhí)行代碼的目標(biāo)函數(shù)。對(duì)于目標(biāo)函數(shù)IrunCodeActually而言,它總是希望獲取的參數(shù)類型是傳入IamForwording時(shí)的參數(shù)類型。這似乎是一件簡(jiǎn)單的事情,實(shí)際并非如此。為何還要進(jìn)行完美轉(zhuǎn)發(fā)呢?因?yàn)橛抑狄帽旧硎莻€(gè)左值,當(dāng)一個(gè)右值引用類型作為函數(shù)的形參,在函數(shù)內(nèi)部再轉(zhuǎn)發(fā)該參數(shù)的時(shí)候它實(shí)際上是一個(gè)左值,并不是它原來(lái)的右值引用類型了??疾烊缦鲁绦颍?/p>

template<typename T>
void PrintT(T& t)
{
 cout << "lvalue" << endl;
}

template<typename T>
void PrintT(T && t)
{
 cout << "rvalue" << endl;
}

template<typename T>
void TestForward(T&& v)
{
 PrintT(v);   
}

int main()
{
 TestForward(1); //輸出lvaue,理應(yīng)輸出rvalue
}

實(shí)際上,我們只需要使用函數(shù)模板std::forward即可完成完美轉(zhuǎn)發(fā),按照參數(shù)本來(lái)的類型轉(zhuǎn)發(fā)出去,考察如下程序:

template<typename T>
void TestForward(T&& v)
{
 PrintT(std::forward<T>(v)); 
}

int main()
{
 TestForward(1); //輸出rvalue
 int x=1;
 TestForward(x); //輸出lvalue
}

下面給出std::forward的簡(jiǎn)單實(shí)現(xiàn):

template<typename T>
struct RemoveReference
{
 typedef T Type;
};

template<typename T>
struct RemoveReference<T&>
{
 typedef T Type;
};

template<typename T>
struct RemoveReference<T&&>
{
 typedef T Type;
};

template<typename T>
constexpr T&& ForwardValue(typename RemoveReference<T>::Type&& value)
{
 return static_cast<T&&>(value);
}

template<typename T>
constexpr T&& ForwardValue(typename RemoveReference<T>::Type& value)
{
 return static_cast<T&&>(value);
}

其中函數(shù)模板ForwardValue就是對(duì)std::forward的簡(jiǎn)單實(shí)現(xiàn)。

2.4關(guān)于引用折疊

C++11中實(shí)現(xiàn)完美轉(zhuǎn)發(fā)依靠的是模板類型推導(dǎo)和引用折疊。模板類型推導(dǎo)比較簡(jiǎn)單,STL中的容器廣泛使用了類型推導(dǎo)。比如,當(dāng)轉(zhuǎn)發(fā)函數(shù)的實(shí)參是類型X的一個(gè)左值引用,那么模板參數(shù)被推導(dǎo)為X&,當(dāng)轉(zhuǎn)發(fā)函數(shù)的實(shí)參是類型X的一個(gè)右值引用的話,那么模板的參數(shù)被推導(dǎo)為X&&類型。再結(jié)合引用折疊規(guī)則,就能確定出參數(shù)的實(shí)際類型。

引用折疊式什么?引用折疊規(guī)則就是左值引用與右值引用相互轉(zhuǎn)化時(shí)會(huì)發(fā)生類型的變化,變化規(guī)則為:

1. T& + & => T&
2. T&& + & => T&
3. T& + && => T&
4. T&& + && => T&&

上面的規(guī)則中,前者代表接受類型,后者代表進(jìn)入類型,=>表示引用折疊之后的類型,即最后被推導(dǎo)決斷的類型。簡(jiǎn)單總結(jié)為:
 (1)所有右值引用折疊到右值引用上仍然是一個(gè)右值引用;
 (2)所有的其他引用類型之間的折疊都將變成左值引用。

通過(guò)引用折疊規(guī)則保留參數(shù)原始類型,完美轉(zhuǎn)發(fā)在不破壞const屬性的前提下,將參數(shù)完美轉(zhuǎn)發(fā)到目的函數(shù)中。

3.右值引用的作用

右值引用的作用是用于移動(dòng)構(gòu)造函數(shù)(Move Constructors)和移動(dòng)賦值運(yùn)算符( Move Assignment Operator)。為了讓我們自己定義的類型支持移動(dòng)操作,我們需要為其定義移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符。這兩個(gè)成員類似對(duì)應(yīng)的拷貝操作,即拷貝構(gòu)造和賦值運(yùn)算符,但它們從給定對(duì)象竊取資源而不是拷貝資源。

移動(dòng)構(gòu)造函數(shù):

 移動(dòng)構(gòu)造函數(shù)類似于拷貝構(gòu)造函數(shù),第一個(gè)參數(shù)是該類類型的一個(gè)右值引用,同拷貝構(gòu)造函數(shù)一樣,任何額外的參數(shù)都必須有默認(rèn)實(shí)參。完成資源移動(dòng)后,原對(duì)象不再保留資源,但移動(dòng)構(gòu)造函數(shù)還必須確保原對(duì)象處于可銷(xiāo)毀的狀態(tài)。

移動(dòng)構(gòu)造函數(shù)的相對(duì)于拷貝構(gòu)造函數(shù)的優(yōu)點(diǎn):移動(dòng)構(gòu)造函數(shù)不會(huì)因拷貝資源而分配內(nèi)存,僅僅接管源對(duì)象的資源,提高了效率。

移動(dòng)賦值運(yùn)算符:

 移動(dòng)賦值運(yùn)算符類似于賦值運(yùn)算符,進(jìn)行的是資源的移動(dòng)操作而不是拷貝操作從而提高了程序的性能,其接收的參數(shù)也是一個(gè)類對(duì)象的右值引用。移動(dòng)賦值運(yùn)算符必須正確處理自賦值。

下面給出移動(dòng)構(gòu)造函數(shù)和移動(dòng)析構(gòu)函數(shù)利用右值引用來(lái)提升程序效率的實(shí)例,首先我先寫(xiě)了一個(gè)山寨的vector:

#include <iostream>
#include <string>
using namespace std;

class Obj
{
public:
 Obj(){cout <<"create obj" << endl;}
 Obj(const Obj& other){cout<<"copy create obj"<<endl;}
};


template <class T> class Container
{
public:
  T* value;
public:
  Container() : value(NULL) {};
  ~Container()
  {
 if(value) delete value; 
 }

 //拷貝構(gòu)造函數(shù)
 Container(const Container& other)
 {
    value = new T(*other.value);
 cout<<"in constructor"<<endl;
  }
 //移動(dòng)構(gòu)造函數(shù)
  Container(Container&& other)
  {
 if(value!=other.value){
  value = other.value;
  other.value = NULL;
 }
 cout<<"in move constructor"<<endl;
  }
 //賦值運(yùn)算符
  const Container& operator = (const Container& rhs) 
  {
 if(value!=rhs.value) 
 {
  delete value;
  value = new T(*rhs.value);
 }
 cout<<"in assignment operator"<<endl;
    return *this;
  }
 //移動(dòng)賦值運(yùn)算符
  const Container& operator = (Container&& rhs)
  {
 if(value!=rhs.value) 
 {
  delete value;
  value=rhs.value;
  rhs.value=NULL;
 }
 cout<<"in move assignment operator"<<endl;
    return *this;
  }

  void push_back(const T& item) 
  {
    delete value;
    value = new T(item);
  }
};

Container<Obj> foo() 
{
 Container<Obj> c;
   c.push_back(Obj());
 cout << "---- exit foo ----" << endl;
   return c;
}

int main() 
{
 Container<Obj> v;
 v=foo(); //采用移動(dòng)構(gòu)造函數(shù)來(lái)構(gòu)造臨時(shí)對(duì)象,再將臨時(shí)對(duì)象采用移動(dòng)賦值運(yùn)算符移交給v
 getchar();
}

程序輸出:

create obj
copy create obj
---- exit foo ----
in move constructor
in move assignment operator

上面構(gòu)造的容器只能存放一個(gè)元素,但是不妨礙演示。從函數(shù)foo中返回容器對(duì)象全程采用移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符,所以沒(méi)有出現(xiàn)元素的拷貝情況,提高了程序效率。如果去掉Container的移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符,程序結(jié)果如下:

create obj
copy create obj
---- exit foo ----
copy create obj
in constructor
copy create obj
in assignment operator

可見(jiàn)在構(gòu)造容器Container的臨時(shí)對(duì)象tmp時(shí)發(fā)生了元素的拷貝,然后由臨時(shí)對(duì)象tmp再賦值給v時(shí),又發(fā)生了一次元素的拷貝,結(jié)果出現(xiàn)了無(wú)謂的兩次元素拷貝,這嚴(yán)重降低了程序的性能。由此可見(jiàn),右值引用通過(guò)移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符來(lái)實(shí)現(xiàn)對(duì)象移動(dòng)在C++程序開(kāi)發(fā)中的重要性。

同理,如果想以左值來(lái)調(diào)用移動(dòng)構(gòu)造函數(shù)構(gòu)造容器Container的話,那么需要將左值對(duì)象通過(guò)std::move來(lái)獲取對(duì)其的右值引用,參考如下代碼:

//緊接上面的main函數(shù)中的內(nèi)容
Container<Obj> c=v;  //調(diào)用普通拷貝構(gòu)造函數(shù),發(fā)生元素拷貝
cout<<"-------------------"<<endl;
Container<Obj> c1=std::move(v); //獲取對(duì)v的右值引用,然后調(diào)用移動(dòng)構(gòu)造函數(shù)構(gòu)造c1
cout<<c1.value<<endl;
cout<<v.value<<endl;   //v的元素值已經(jīng)在動(dòng)構(gòu)造函數(shù)中被置空(被移除)

代碼輸出:

copy create obj
in constructor
-------------------
in move constructor
00109598
00000000

以上是c++11新標(biāo)準(zhǔn)中移動(dòng)語(yǔ)義與右值引用是什么的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對(duì)大家有幫助,更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI