程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 深刻解讀C++中的右值援用

深刻解讀C++中的右值援用

編輯:關於C++

深刻解讀C++中的右值援用。本站提示廣大學習愛好者:(深刻解讀C++中的右值援用)文章只能為提供參考,不一定能成為您想要的結果。以下是深刻解讀C++中的右值援用正文


右值援用(及其支撐的Move語意和完善轉發)是C++0x將要參加的最嚴重說話特征之一,這點從該特征的提案在C++ - State of the Evolution列表上高居榜首也能夠看得出來。
從理論角度講,它可以或許完善處理C++中久長以來為人所诟病的暫時對象效力成績。從說話自己講,它健全了C++中的援用類型在左值右值方面的缺點。從庫設計者的角度講,它給庫設計者又帶來了一把利器。從庫應用者的角度講,不動一兵一卒即可以取得“收費的”效力晉升…
在尺度C++說話中,暫時量(術語為右值,因其湧現在賦值表達式的左邊)可以被傳給函數,但只能被接收為const &類型。如許函數便沒法辨別傳給const &的是真實的右值照樣慣例變量。並且,因為類型為const &,函數也沒法轉變所傳對象的值。C++0x將增長一種名為右值援用的新的援用類型,記作typename &&。這類類型可以被接收為非const值,從而許可轉變其值。這類轉變將許可某些對象創立轉移語義。好比,一個std::vector,就其外部完成而言,是一個C式數組的封裝。假如須要創立vector暫時量或許從函數中前往vector,那就只能經由過程創立一個新的vector並拷貝一切存於右值中的數據來存儲數據。以後這個暫時的vector則會被燒毀,同時刪除其包括的數據。有了右值援用,一個參數為指向某個vector的右值援用的std::vector的轉移結構器就可以夠簡略地將該右值中C式數組的指針復制到新的vector,然後將該右值清空。這裡沒稀有組拷貝,而且燒毀被清空的右值也不會燒毀保留數據的內存。前往vector的函數如今只須要前往一個std::vector<>&&。假如vector沒有轉移結構器,那末成果會像之前一樣:用std::vector<> &參數挪用它的拷貝結構器。假如vector確切具有轉移結構器,那末轉移結構器就會被挪用,從而防止年夜量的內存分派。

一. 界說
平日意義上,在C++中,可取地址,著名字的即為左值。弗成取地址,沒著名字的為右值。右值重要包含字面量,函數前往的暫時變量值,表達式暫時值等。右值援用即為對右值停止援用的類型,在C++98中的援用稱為左值援用。
若有以下類和函數:

class A
{
private:
 int* _p;
};

A ReturnValue()
{
 return A();
}


ReturnValue()的前往值即為右值,它是一個不簽字的暫時變量。在C++98中,只要常量左值援用能力援用這個值。
A& a = ReturnValue(); // error: non-const lvalue reference to type 'A' cannot bind to a temporary of type 'A'
   
const A& a2 = ReturnValue(); // ok

經由過程常量左值援用,可以延伸ReturnValue()前往值的性命周期,然則不克不及修正它。C++11的右值援用進場了:
A&& a3 = ReturnValue();

右值援用經由過程”&&”來聲明, a3援用了ReturnValue()的前往值,延伸了它的性命周期,而且可以對該暫時值停止修正。

二. 挪動語義
右值援用可以援用並修正右值,然則平日情形下,修正一個暫時值是沒成心義的。但是在對暫時值停止拷貝時,我們可以經由過程右值援用來將暫時值外部的資本移為己用,從而防止了資本的拷貝:

#include<iostream>

class A
{
public:
 A(int a)
 :_p(new int(a))
 {
 }

 // 挪動結構函數 挪動語義
 A(A&& rhs)
 : _p(rhs._p)
 {
 // 將暫時值資本置空 防止屢次釋放 如今資本的歸屬權曾經轉移
 rhs._p = nullptr; 
 std::cout<<"Move Constructor"<<std::endl;
 }
 // 拷貝結構函數 復制語義
 A(const A& rhs)
 : _p(new int(*rhs._p))
 {
 std::cout<<"Copy Constructor"<<std::endl;
 }
 
private:
 int* _p;
};

A ReturnValue() { return A(5); }

int main()
{
 A a = ReturnValue();
 return 0;
}

運轉該代碼,發明Move Constructor被挪用(在g++中會對前往值停止優化,不會有任何輸入。可以經由過程-fno-elide-constructors封閉這個選項)。在用右值結構對象時,編譯器會挪用A(A&& rhs)情勢的挪動結構函數,在挪動結構函數中,你可以完成本身的挪動語義,這裡將暫時對象中_p指向內存直接移為己用,防止了資本拷貝。當資本異常年夜或結構異常耗不時,效力晉升將異常顯著。假如A沒有界說挪動結構函數,那末像在C++98中那樣,將挪用拷貝結構函數,履行拷貝語義。挪動不成,還可以拷貝。
std::move:
C++11供給一個函數std::move()來將一個左值強迫轉化為右值:

A a1(5);
A a2 = std::move(a1);

下面的代碼在結構a2時將會挪用挪動結構函數,而且a1的_p會被置空,由於資本曾經被挪動了。而a1的性命周期和感化域並沒有變,依然要比及main函數停止後再析構,是以以後對a1的_p的拜訪將招致運轉毛病。
std::move乍一看沒甚麼用。它重要用在兩個處所:
  • 贊助更好地完成挪動語義
  • 完成完善轉發(上面會提到)

斟酌以下代碼:

class B
{
public:
 B(B&& rhs)
 : _pb(rhs._pb)
 {
 // how can i move rhs._a to this->_a ?
 rhs._pb = nullptr;
 }

private:
 A _a;
 int * pb;
}


關於B的挪動結構函數來講,因為rhs是右值,行將被釋放,是以我們不只願望將_pb的資本挪動過去,還願望應用A類的挪動結構函數,將A的資本也履行挪動語義。但是成績出在假如我們直接在初始化列表中應用:_a(rhs._a) 將挪用A的拷貝結構函數。由於參數 rhs._a 此時是一個簽字值,而且可以取址。現實上,B的挪動結構函數的參數rhs也是一個左值,由於它也簽字,而且可取址。這是在C++11右值援用中讓人很困惑的一點:可以接收右值的右值援用自己倒是個左值
這一點在前面的完善轉發回會提到。如今我們可以用std::move來將rhs._a轉換為右值:_a(std::move(rhs._a)),如許將挪用A的挪動結構。完成挪動語義。固然這裡我們確信rhs._a以後不會在應用,由於rhs行將被釋放。

三. 完善轉發
假如僅僅為了完成挪動語義,右值援用是沒有需要被提出來的,由於我們在挪用函數時,可以經由過程傳援用的方法來防止暫時值的生成,雖然代碼不是那末直不雅,但效力比應用右值援用只高不低。
右值援用的另外一個感化是完善轉發,完善轉收回如今泛型編程中,將模板函數參數傳遞給該函數挪用的下一個模板函數。如:

template<typename T>
void Forward(T t)
{
 Do(t);
}

下面的代碼中,我們願望Forward函數將傳入參數類型原封不動地傳遞給Do函數,即Forward函數吸收的左值,則Do吸收到左值,Forward吸收到右值,Do也將獲得右值。下面的代碼可以或許准確轉發參數,然則是不完善的,由於Forward吸收參數時履行了一次拷貝。
斟酌到防止拷貝,我們可以傳遞援用,形如Forward(T& t),然則這類情勢的Forward其實不能吸收右值作為參數,如Forward(5)。由於異常量左值不克不及綁定到右值。斟酌常量左值援用:Forward(const T& t),這類情勢的Forward可以或許吸收任何類型(常量左值援用是全能援用),然則因為加上了常量潤飾符,是以沒法准確轉發異常量左值援用:
void Do(int& i)
{
 // do something...
}

template<typename T>
void Forward(const T& t)
{
 Do(t);
}

int main()
{
 int a = 8;
 Forward(a); // error. 'void Do(int&)' : cannot convert argument 1 from 'const int' to 'int&'
 return 0;
}

基於這類情形, 我們可以對Forward的參數停止const重載,便可准確傳遞左值援用。然則當Do函數參數為右值援用時,Forward(5)依然不克不及准確傳遞,由於Forward中的參數都是左值援用。
上面引見在 C++11 中的處理計劃。
PS:援用折疊
C++11引入了援用折疊規矩,聯合右值援用來處理完善轉提問題:

typedef const int T;
typedef T& TR;
TR& v = 1; // 在C++11中 v的現實類型為 const int&

如上代碼中,產生了援用折疊,將TR睜開,獲得 T& & v = 1(留意這裡不是右值援用)。 這裡的 T& + & 被折疊為 T&。更加具體的,依據TR的類型界說,和v的聲明,產生的折疊規矩以下:
T& + &  = T&
T& + && = T&
T&& + &  = T&
T&& + && = T&&

下面的規矩被簡化為:只需湧現左值援用,規矩老是優先折疊為左值援用。僅當湧現兩個右值援用才會折疊為右值援用。
再談轉發
那末下面的援用折疊規矩,對完善轉發有甚麼用呢?我們留意到,關於T&&類型,它和左值援用折疊為左值援用,和右值援用折疊為右值援用。基於這類特征,我們可以用 T&& 作為我們的轉發函數模板參數:
template<typename T>
void Forward(T&& t)
{
 Do(static_cast<T&&>(t));
}

如許,不管Forward吸收到的是左值,右值,常量,異常量,t都能堅持為其准確類型。
當傳入左值援用 X& 時:
void Forward(X& && t)
{
 Do(static_cast<X& &&>(t));
}

折疊後:
void Forward(X& t)
{
 Do(static_cast<X&>(t));
}

這裡的static_cast看起來仿佛是沒有需要,而它現實上是為右值援用預備的:
void Forward(X&& && t)
{
 Do(static_cast<X&& &&>(t));
}

折疊後:
void Forward(X&& t)
{
 Do(static_cast<X&&>(t));
}

後面提到過,可以吸收右值的右值援用自己倒是個左值,由於它簽字而且可以取值。是以在Forward(X&& t)中,參數t曾經是一個左值了,此時我們須要將其轉換為它自己傳入的類型,即為右值。因為static_cast中援用折疊的存在,我們總能復原參數原來的類型。
在C++11中,static_cast<T&&>(t) 可以經由過程 std::forward<T>(t) 來替換,std::forward是C++11用於完成完善轉發的一個函數,它和std::move一樣,都經由過程static_cast來完成。我們的Forward函數終究釀成了:
template<typename T>
void Forward(T&& t)
{
 Do(std::forward<T>(t));
}

可以經由過程以下代碼來測試:
#include<iostream>
using namespace std;

void Do(int& i)    { cout << "左值援用"  << endl; }
void Do(int&& i)   { cout << "右值援用"  << endl; }
void Do(const int& i) { cout << "常量左值援用" << endl; }
void Do(const int&& i) { cout << "常量右值援用" << endl; }

template<typename T>
void PerfectForward(T&& t){ Do(forward<T>(t)); }

int main()
{
 int a;
 const int b;
 
 PerfectForward(a);  // 左值援用
 PerfectForward(move(a)); // 右值援用
 PerfectForward(b);  // 常量左值援用
 PerfectForward(move(b)); // 常量右值援用
 return 0;
}

四. 附注
左值和左值援用,右值和右值援用都是統一個器械,援用不是一個新的類型,僅僅是一個體名。這一點關於懂得模板推導很主要。關於以下兩個函數

template<typename T>
void Fun(T t)
{
 // do something...
}

template<typename T>
void Fun(T& t)
{
 // do otherthing...
}

Fun(T t)和Fun(T& t)他們都能接收左值(援用),它們的差別在於對參數作分歧的語義,前者履行拷貝語義,後者只是取個新的別號。是以挪用Fun(a)編譯器會報錯,由於它不曉得你要對a履行何種語義。別的,關於Fun(T t)來講,因為它履行拷貝語義,是以它還能接收右值。是以挪用Fun(5)不會報錯,由於左值援用沒法援用到右值,是以只要Fun(T t)能履行拷貝。
最初,附上VS中 std::move 和 std::forward 的源碼:

// move
template<class _Ty> 
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{ 
 return ((typename remove_reference<_Ty>::type&&)_Arg);
}

// forward
template<class _Ty> 
inline _Ty&& forward(typename remove_reference<_Ty>::type& _Arg)
{ // forward an lvalue
 return (static_cast<_Ty&&>(_Arg));
}

template<class _Ty> 
inline _Ty&& forward(typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT
{ // forward anything
 static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");
 return (static_cast<_Ty&&>(_Arg));
}

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved