日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

《C++0x漫谈》系列之:右值引用

發布時間:2025/3/15 c/c++ 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《C++0x漫谈》系列之:右值引用 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
右值引用(及其支持的Move語意和完美轉發)是C++0x將要加入的最重大語言特性之一,這點從該特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出來。從實踐角度講,它能夠完美解決C++中長久以來為人所詬病的臨時對象效率問題。從語言本身講,它健全了C++中的引用類型在左值右值方面的缺陷。從庫設計者的角度講,它給庫設計者又帶來了一把利器。從庫使用者的角度講,不動一兵一卒便可以獲得“免費的”效率提升…

   Move語意

  返回值效率問題——返回值優化((N)RVO)——mojo設施——workaround——問題定義——Move語意——語言支持

  大猴子Howard Hinnant寫了一篇挺棒的tutorial(a.k.a. 提案N2027),此外最初的關于rvalue-reference的若干篇提案的可讀性也相當強。因此要想了解rvalue-reference的話,或者去看C++標準委員會網站上的系列提案(見文章末尾的參考文獻)。或者閱讀本文。

   源起

  《大史記》總看過吧?

  故事,素介個樣子滴…一天,小嗖風風的吹著,在一個伸手不見黑夜的五指…

  我用const引用來接受參數,卻把臨時變量一并吞掉了。我用非const引用來接受參數,卻把const左值落下了。于是乎,我就在標準的每個角落尋找解決方案,我靠!我被8.5.3打敗了!…

  設想這樣一段代碼(既然大同小異,就直接從Andrei那篇著名的文章里面拿來了):

std::vector<int> v = readFile();

  readFile()的定義是這樣的:

std::vector<int> readFile()
{
std::vector<int> retv;
… // fill retv
return retv;
}

  這段代碼低效的地方在于那個返回的臨時對象。一整個vector得被拷貝一遍,僅僅是為了傳遞其中的一組int,當v被構造完畢之后,這個臨時對象便煙消云散。

  這完全是公然的浪費!

  更糟糕的是,原則上講,這里有兩份浪費。一,retv(retv在readFile()結束之后便煙消云散)。二,返回的臨時對象(返回的臨時變量在v拷貝構造完畢之后也隨即香消玉殞)。不過呢,對于上面的簡單代碼來說,大部分編譯器都已經能夠做到優化掉這兩個對象,直接把那個retv創建到接受返回值的對象,即v中去。

  實際上,臨時對象的效率問題一直是C++中的一個被廣為詬病的問題。這個問題是如此的著名,以至于標準不惜犧牲原本簡潔的拷貝語意,在標準的12.8節悍然下詔允許優化掉在函數返回過程中產生的拷貝(即便那個拷貝構造函數有副作用也在所不惜!)。這就是所謂的“Copy Elision”。

  為什么(N)RVO((Named) Return Value Optimization)幾乎形同虛設

  還是按照Andrei的說法,只要readFile()改成這樣:

… readFile()
{
if(/* err condition */) return std::vector<int>();
if(/* yet another err condition */) return std::vector<int>(1, 0);
std::vector<int> retv;
… // fill retv
return retv;
}

  出現這種情況,編譯器一般都會乖乖放棄優化。

  但對編譯器來說這還不是最郁悶的一種情況,最郁悶的是:

std::vector<int> v;
v = readFile(); // assignment, not copy construction

  這下由拷貝構造,變成了拷貝賦值。眼睛一眨,老母雞變鴨。編譯器只能繳械投降。因為標準只允許在拷貝構造的情況下進行(N)RVO。

   為什么庫方案也不是生意經

  C++鬼才Andrei Alexandrescu以對C++標準的深度挖掘和利用著名,早在03年的時候(當時所謂的臨時變量效率問題已經在新聞組上鬧了好一陣子了,相關的語言級別的解決方案也已經在02年9月份粉墨登場)就在現有標準(C++98)下硬是折騰出了一個能100%解決問題的方案來。

  Andrei把這個框架叫做mojo,就像一層爽身粉一樣,把它往現有類上面一灑,嘿嘿…猜怎么著,不,不是“痱子去無蹤”:P,是該類型的臨時對象效率問題就迎刃而解了!

  Mojo的唯一的問題就是使用方法過于復雜。這個復雜度,很大程度上來源于標準中的一個措辭問題(C++標準就是這樣,鬼知道哪個角落的一句話能夠帶出一個brilliant的解決方案來,同時,鬼知道哪個角落的一句話能夠抹殺一個原本簡潔的解決方案)。這個問題就是我前面提到過的8.5.3問題,目前已經由core language issue 391解決。

  對于庫方案來說,解決問題固然是首要的。但一個侵入性的,外帶使用復雜性的方案必然是走不遠的。因此雖然大家都不否認mojo是一個天才的方案,但實際使用中難免舉步維艱。這也是為什么mojo并沒有被工業化的原因。

  為什么改用引用傳參也等于癡人說夢

void readFile(vector<int>& v){ … // fill v }

  這當然可以。

  但是如果遇到操作符重載呢?

string operator+(string const& s1, string const& s2);

  而且,就算是對于readFile,原先的返回vector的版本支持

BOOST_FOREACH(int i, readFile()){
… // do sth. with i
}

  改成引用傳參后,原本優雅的形式被破壞了,為了進行以上操作不得不引入一個新的名字,這個名字的存在只是為了應付被破壞的形式,一旦foreach操作結束它短暫的生命也隨之結束:

vector<int> v;
readFile(v);
BOOST_FOREACH(int I, v){
}

// v becomes useless here

  還有什么問題嗎?自己去發現吧。總之,利用引用傳參是一個解決方案,但其能力有限,而且,其自身也會帶來一些其它問題。終究不是一個優雅的辦法。

   問題是什么

  《你的燈亮著嗎?》里面漂亮地闡述了定義“問題是什么”的重要性。對于我們面臨的臨時對象的效率問題,這個問題同樣重要。

  簡而言之,問題可以描述為:

  C++沒有區分copy和move語意。

  什么是move語意?記得auto_ptr嗎?auto_ptr在“拷貝”的時候其實并非嚴格意義上的拷貝。“拷貝”是要保留源對象不變,并基于它復制出一個新的對象出來。但auto_ptr的“拷貝”卻會將源對象“掏空”,只留一個空殼——一次資源所有權的轉移。

  這就是move。

   Move語意的作用——效率優化

  舉個具體的例子,std::string的拷貝構造函數會做兩件事情:一,根據源std::string對象的大小分配一段大小適當的緩沖區。二,將源std::string中的字符串拷貝過來。

// just for illustrating the idea, not the actual implementation

string::string(const string& o)
{
this->buffer_ = new buffer[o.length() + 1];
copy(o.begin(), o.end(), buffer_);
}

  但是假設我們知道o是一個臨時對象(比如是一個函數的返回值),即o不會再被其它地方用到,o的生命期會在它所處的full expression的結尾結束的話,我們便可以將o里面的資源偷過來:

string::string(temporary string& o)
{
// since o is a temporary, we can safely steal its resources without causing any problem
this->buffer_ = o.buffer_;
o.buffer_ = 0;
}

  這里的temporary是一個捏造的關鍵字,其作用是使該構造函數區分出臨時對象(即只有當參數是一個臨時的string對象時,該構造函數才被調用)。

  想想看,如果存在這樣一個move constructor(搬移式構造函數)的話,所有源對象為臨時對象的拷貝構造行為都可以簡化為搬移式(move)構造。對于上面的string例子來說,move和copy construction之間的效率差是節省了一次O(n)的分配操作,一次O(n)的拷貝操作,一次O(1)的析構操作(被拷貝的那個臨時對象的析構)。這里的效率提升是顯而易見且顯著的。

  最后,要實現這一點,只需要我們具有判斷左值右值的能力(比如前面設想的那個temporary關鍵字),從而針對源對象為臨時對象的情況進行“偷”資源的行動。

   Move語意的作用——使能(enabling)

  再舉一個例子,std::fstream。fstream是不可拷貝的(實際上,所有的標準流對象都是不可拷貝的),因而我們只能通過引用來訪問一開始建立的那個流對象。但是,這種辦法有一個問題,如果我們要從一個函數中返回一個流對象出來就不行了:

// how do we make this happen?
std::fstream createStream()
{ … }


  當然,你可以用auto_ptr來解決這個問題,但這就使代碼非常笨拙且難以維護。

  但如果fstream是moveable的,以上代碼就是可行的了。所謂“moveable”即是指(當源對象是臨時對象時)在對象拷貝語法之下進行的實際動作是像auto_ptr那樣的資源所有權轉移:源對象被掏空,所有資源都被轉移到目標對象中——好比一次搬家(move)。move操作之后,源對象雖然還有名有姓地存在著,但實際上其“實質”(內部擁有的資源)已經消失了,或者說,源對象從語意上已經消失了。

  對于moveable但并非copyable的fstream對象來說,當發生一次move時(比如在上面的代碼中,當一個局部的fstream對象被move出createStream()函數時),不會出現同一對象的兩個副本,取而代之的是,move的源對象的身份(Identity)消失了,這個身份由返回的臨時fstream對象重新持有。也就是說,fstream的唯一性(不可拷貝性——non-copyable)得到了尊重。

  你可能會問,那么被搬空了的那個源對象如果再被使用的話豈不是會引發問題?沒錯。這就是為什么我們應該僅當需要且可以去move一個對象的時候去move它,比如在函數的最后一行(return)語句中將一個局部的vector對象move出來(return std::move(v)),由于這是最后一行語句,所以后面v不可能再被用到,對它來說所剩下的操作就是析構,因此被掏空從語意上是完全恰當的。

?  最初的例子——完美解決方案

  在先前的那個例子中

vector<int> v = readFile();

  有了move語意的話,readFile就可以簡單的改成:

std::vector<int> readFile()
{
std::vector<int> retv;
… // fill retv
return std::move(retv); // move retv out
}

  std::move以后再介紹。目前你只要知道,std::move就可以把retv掏空,即搬移出去,而搬家的最終目的地是v。這樣的話,從內存分配的角度講,只有retv中進行的內存分配,在從retv到返回的臨時對象,再從后者到目的地v的“move”過程中,沒有任何的內存分配(我是指vector內的緩沖區分配),取而代之的是,先是retv內的緩沖區被“轉移”到返回值臨時對象中,然后再從臨時對象中轉移到v中。相比于以前的兩次拷貝而言,兩次move操作節省了多少工作量呢?節省了兩次new操作兩次delete操作,還有兩次O(n)的拷貝操作,這些操作整體的代價正比于retv這個vector的大小。難怪人們說臨時對象效率問題是C++的腫瘤(wart)之一,難怪C++標準都要不惜代價允許(N)RVO。

   如何支持move語意

  根據前面的介紹,你想必已經知道。實現move語意的最關鍵環節在于能夠在編譯期區分左值右值(也就是說識別出臨時對象)。

  現在,回憶一下,在文章的開頭我曾經提到:

  我用const引用來接受參數,卻把臨時變量一并吞掉了。我用非const引用來接受參數,卻把const左值落下了。于是乎,我就在標準的每個角落尋找解決方案,我靠!我被8.5.3打敗了!…

  為什么這么說?

   現行標準(C++03)下的方案

  要想區分左值右值,只有通過重載:

void foo(X const&);
void foo(X&);

  這樣的重載顯然是行不通的。因為X const&會把non-const臨時對象一并吞掉。

  這種做法的問題在于。X&是一個non-const引用,它只能接受non-const左值。然而,C++里面的值一共有四種組合:

  const non-const
  lvalue
  rvalue

  常量性(const-ness)與左值性(lvalue-ness)是正交的。

  non-const引用只能綁定到其中的一個組合,即non-const lvalue。還剩下const左值,const右值,以及我們最關心的——non-const右值。而只有最后一種——non-const右值——才是可以move的。

  剩下的問題便是如何設計重載函數來搞定const左值和const右值。使得最后只留下non-const右值。

  所幸的是,我們可以借助強大的模板參數推導機制:

// catch non-const lvalues
void foo(X&);
// catch const lvalues and const rvalues
template<typename T>
void foo(T&, enable_if_same<T, const X>::type* = 0);
void foo( /* what goes here? */);

  注意,第二個重載負責接受const左值和const右值。經過第一第二個foo重載之后剩下來的便是non-const rvalue了。

  問題是,我們怎么捕獲這些non-const rvalue呢?根據C++03,const-const rvalue只能綁定到const引用。但如果我們用const引用的話,就會越俎代庖把const左右值一并接受了(因為在模板函數(第二個重載)和非模板函數(第三個重載)之間編譯器總是會偏好非模板)。

  那除了用const引用,難道還有什么辦法來接受一個non-const rvalue嗎?

  有。

  假設你的類型為X,那么只要在X里面加入一點料:

struct ref_x
{
ref_x(X* p) : p_(p) {}
X* p_;
};
struct X
{
// original stuff

// added stuff, for move semantic
operator ref_x()
{
return ref_x(this);
}
};

  這樣,我們的第三個重載函數便可以寫成:

void foo(ref_x rx); // accept non-const temporaries only!

  Bang! 我們成功地在C++03下識別出了moveable的non-const臨時對象。不過前提是必須得在moveable的類型里加入一些東西。這也正是該方案的最大弊病——它是侵入式的(姑且不說它利用了語言的陰暗角落,并且帶來了很大的編碼復雜度)。

   C++09的方案

  實際上,剛才講的這個利用重載的方案做成庫便是Andrei的mojo框架。mojo框架固然精巧,但復雜性太大,使用成本太高,不夠優雅直觀。所以語言級別的支持看來是必然選擇(后面你還會看到,為了支持move語意而引入的新的語言特性同時還支持了另一個廣泛的問題——完美轉發)。

  C++03之所以讓人費神就是因為它沒有一個引用類型來綁定到右值,而是用const左值引用來替代,事實證明這個權宜之計并不是長遠之道,時隔10年,終歸還是要健全引用的左右值語意。

  C++09加入一個新的引用類型——右值引用。右值引用的特點是優先綁定到右值。其語法是&&(注意,不讀作“引用的引用”,讀作“右值引用”)。有了右值引用,我們前面的方案便可以簡單的修改為:

void foo(X const& x);
void foo(X&& x);

  這樣一來,左值以及const右值都被綁定到了第一個重載版本。剩下的non-const右值被綁定到第二個重載版本。

  對于你的moveable的類型X,則是這樣:

struct X
{
X();
X(X const& o); // copy constructor
X(X&& o); // move constructor
};

X source();
X x = source(); // #1

  在#1處,調用的將會是X::X(X&& o),即所謂的move constructor,因為source()返回的是一個臨時對象(non-const右值),重載決議會選中move constructor。

總結

以上是生活随笔為你收集整理的《C++0x漫谈》系列之:右值引用的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。