與臨時對象的鬥爭(上)ZZ

C++ 是一門以效率見長的語言(雖然近來愈來愈多的人「不齒」談及效率,我深覺得否則,在某一次的程序編寫中不對效率錙銖必較並不意味意味着咱們就不該該追求更多的更好的作法)。總之吧,相比起其它語言,程序員們在使 C++ 的時候會更加有意識地去避免沒有效率的作法。在C++ 的程序中,臨時對象的產生就是損及效率的「惡因」之一,所以也產生出一些意思的技術和優化手段,這篇文章裏我總結一下最近在這些方面學習的一些收穫:html

返回值優化(RVO)與具命返回值優化(NRVO)

這是一項編譯器作的優化,已是一種很常見的優化手段了,放狗搜一下能夠找到不少的資料,在 MSDN 裏也有相關的說明。程序員

返回值優化,顧名思義,就是與返回值有關的優化(廢話……),是當函數是按值返回(而不是引用啊、指針啊)時,爲了不產生沒必要要的臨時對象以及值拷貝而進行的優化。編程

先看看下面的代碼:多線程

typedef unsigned int UINT32; class MyCla { public: MyCla(UINT32 a_size = 10):size(a_size) { p = new UINT32[size]; } MyCla(MyCla const & a_right):size(a_right.size) { p = new UINT32[size]; memcpy(p, a_right.p, size*sizeof(UINT32)); } MyCla const& operator = (MyCla const & a_right) { size = a_right.size; p = new UINT32[size]; memcpy(p, a_right.p, size*sizeof(UINT32)); return *this; } ~MyCla() { delete [] p; } private: UINT32 *p; UINT32 size; }; MyCla TestFun() { return MyCla(); } int _tmain(int argc, _TCHAR* argv[]) { MyCla a = TestFun(); return 0; }

TestFun() 函數返回了一個 MyCla 對象,並且是按值傳遞的。函數

在沒有任何「優化」以前,這段代碼的行爲也許是這樣的:return MyCla() 這行代碼中,構造了一個 MyCla 類的臨時的無名對象(姑且叫它t1),接着把 t1 拷貝到另外一塊臨時對象 t2(不在棧上),而後函數保存好 t2 的地址(放在 eax 寄存器中)後返回,TestFun 的棧區間被「撤消」(這時 t1 也就「沒有」了,t1 的生存域在 TestFun 中,因此被析構了),在 MyCla a = TestFun(); 這一句中,a 利用 t2 的地址,能夠找到 t2 進行,接着進行構造。這樣 a 的構造過程就完成了。而後再把 t2 也「幹掉」。學習

能夠看到,在這個過程當中,t1 和 t2 這兩個臨時的對象的存在實在是很浪費的,佔用空間不說,關鍵是他們都只是爲a的構造而存在,a構造完了以後生命也就終結了。既然這兩個臨時的對象對於程序員來講根本就「看不到、摸不着」(匿名對象嘛,你怎麼引用?),因而編譯器乾脆在裏面作點手腳,不生成它們!怎麼作呢?很簡單,編譯器「偷偷地」在咱們寫的fun函數中增長一個參數 A&,而後把 a 的地址傳進去(注意,這個時候 a 的內存空間已經存在了,但對象尚未被「構造」,也就是構造函數尚未被調用),而後在函數體內部,直接用 a 來代替原來的「匿名對象」,在函數體內部就完成 a 的構造。這樣,就省下了兩個臨時變量的開銷。這就是所謂的「返回值優化」~!在 VC7 裏,按值返回匿名對象時,默認都是這麼作。優化

上面說的是「返回值優化(RVO)」,還有一種「具名返回值優化(NRVO)」,是對於按值返回「具名對象」(就是有名字的變量!)時的優化手段,其實道理是同樣的,但因爲返回的值是具名變量,狀況會複雜不少,因此,能執行優化的條件更苛刻,在下面三種狀況下(來自MSDN),NRVO 將必定不起做用:this

  1. 不一樣的返回路徑上返回不一樣名的對象(好比if XXX 的時候返回x,else的時候返回y)
  2. 引入 EH 狀態的多個返回路徑(就算全部的路徑上返回的都是同一個具名對象)
  3. 在內聯asm語句中引用了返回的對象名。

不過就算 NRVO 不能進行,在上面的描述中的 t2 這個臨時變量也不會產生,對於 VC 的 C++ 編譯器來講,只要你寫的程序是把對象按值返回的,它會有兩種作法,來避免 t2 的產生。拿下面這個程序來講明:spa

MyCla TestFun2() {
    MyCla x(3); return x; }

一種作法是像 RVO同樣,把做爲表達式中獲取返回值來進行構造的變量 a 當成一個引用參數傳入函數中,而後在返回語句以前,用要返回的那個變量來拷貝構造 a,而後再把這個變量析構,函數返回原調用點,a 就構造好了。線程

還有一種方式,是在函數返回的時候,不析構 x ,而直接把 x 的地址放到 exa 寄存器中,返回調到 TestFun2 的調用點上,這時,a 能夠用 exa 中存着的地址來進行構造,a 構造完成以後,再析構原來的變量 x !是的,注意到其實這時,x 的生存域已經超出了 TestFun2,但因爲這裏 x 所在 TestFun2 的棧雖然已經無效,可是並無誰去擦寫這塊存,因此 x 其實仍是有效的,固然,一切都在彙編的層面,對於 C++ 語言層面來說是透明的。

嗯,(具名)返回值引用大約就是這麼多,在網上和 MSDN 上還能查到更多的例子和解釋,對於在多線程下  (N)RVO 須要注意什麼,嗯,我徹底沒有多線程的經驗,不敢亂寫誤人子弟……

右值引用與 move 語意

 

「C++ 中臨時對象對效率產生的影響一直爲人所詬病」(網上流傳的說法),NRVO 等手段也只有在必定程度上彌補這個不足(你知道,在不少狀況下沒法作優化)。在 C++98 肯定後的十多年時間後,「Cpper神聖」們終於給出了另外一個對付它的法寶——右值引用。

對於右值引用,目前我所見過的最好的講解是VC開發團隊blog中發佈的一篇長文(看這裏),在CPP blog上飄飄白雲的博主進行了全文翻譯(譯得很棒),建議細讀三遍!理解裏面每個例子~這樣至少你在右值引用的認識上就有了良好的基礎了。(嗯,我只讀了兩遍,下面說的東西有錯誤的話請原諒並指出 :) )

簡單的說,在C++中的左值,就是能取地址的表達式,好比var、++var之類的,右值就不是能取地址的表達式啦,好比常數 12三、x++、x+y等等。

嗯,咱們能夠看到,右值經常就表明着臨時對象,也就經常意味着「被詬病的浪費……」

好比,z = x + y,這裏,翻譯得更「低層」一點,那麼這裏將是:

temp = x + y z = temp

這個temp是很尷尬的,不用它將沒法實現正確、良好的 operator + 語意,用它就很難避免臨時對象產生的不良開銷。

咱們回到上面 RVO 中的程序例子:

MyCla TestFun() {
    return MyCla(); }

看,這裏返回的 MyCla(),正是一個右值(咱們就給它取個名吧,否則很差稱呼它,嗯,還叫 t1 吧)。在函數返回後,這個 t1 就被析構,它作的析構動做就是把原來申請的內存還給系統。想一想在這以前,a 在幹什麼?a 在構造的時候向系統申請了一塊內存!一個申請,一個還回,一來一回多費事啊,若是能直接把 t1 擁有的內存給 a ,就不省事了嗎?反正 t1 立刻就要掛了。好,右值引用給了咱們這種機會,咱們爲 MyCla 實現一個 move 語意的拷貝構造函數(不知道什麼是 move 拷貝構造?回頭看上面連接的文章三遍!):

MyCla(MyCla && a_right):size(a_right.size) { p = a_right.p; a_right.p = NULL; }

當編譯器探知用於構造 a 的是一個右值時,就調用這個 move 構造函數,而後咱們在這個函數裏偷樑換柱,把 t1 的資源竊取過來了。這樣,就算不使用 RVO,這個構造的開銷也是很是小的。

那麼,對於像:

MyCla TestFun2() {
    MyCla x(3); return x; }

這樣的狀況呢?是的,這裏的 x 是一個左值,不會調用 move 構造函數。但是咱們知道這個 x 其實立刻也要掛了,它的資源不給白不給啊對不對?因此,咱們就想告訴編譯器,您就把它當成個右值吧,怎麼告訴它呢?用 std::move 來實現這種 move 語意,像下面這樣:

MyCla TestFun2() {
    MyCla x(3); return std::move(x); }

好啦,這樣又能用上 MyCla 的 move 構造函數啦。

總結一下,做爲右值的臨時對象,其實它的存在就是充當一個傳遞的橋樑,一旦表達式過了這個橋,那麼這個臨時對象的存在就沒有意義了,也沒有人能再用到它(由於它是個右值,沒有名字,又不能取地址)。既然如此,一個無人問津的就要「死」的變量,把它擁的的資源搶過來也不算過份吧……。在 C++0x 以前,咱們想這麼作,可是沒有手段,雖然編譯器能分清楚左值右值,但咱們沒法經過程序告訴編譯器,若是這是左值,請用這個方法,這個是右值,嘿嘿,那用另外一個方法幫我搶它的資源吧……,。到了 C++0x ,咱們有手段了,那就是右值引用,這個右值引用能夠參與函數的重載,這樣就給了咱們機會,針對左右值分別提供不一樣的操做方法(函數)讓編譯器幫咱們選擇一個合適的。

通常來講,可能須要注意右值引用的地方有:

當咱們寫的類裏擁有動態申請的資源時,那麼老是應該提供一個move構造函數,這將會帶給不少好處,可讓這個類的使用者(通常是咱們本身函數,或是SDL等庫)利用它來提高效率。

若是咱們寫的函數須要利用傳入的(含有動態申請資源的)對象參數來構造新的變量時,咱們能夠提供右值引用的重載版本,並在構造新對象時使用std::move來竊取臨時對象的資源。

右值引用在泛型編程中也有極爲重要的做用(它能實現完美轉發),但和本文沒多大關係,就很少說了。

總之,右值引用是 C++0x 中很是耀眼的一個新的語言特性,VC2010已經將其列入支持範圍(GCC 本人幾乎沒用過,沒了解,不敢妄言[注{ThanksTo OwnWaterloo}:gcc新版本也支持了。 gcc4.4.0 的stl已經加上對move的支持了])。

從實踐的角度講,它可以完美地解決 C++ 中長久以來爲人們所詬病的臨時對象的效率問題。從語言自己來說,它健全了 C++ 中引用類型在左值右值方面原先的缺陷,從庫的設計者角度講,它給設計者又帶來了一把利器。而對於廣大的庫使用者而言,不動一兵一卒便能得到「免費」的效率提高。

牛吧!這個特性如此重要如此有用,幾乎能夠想見在支持右值的編譯器一旦實用化,就將產生大量的使用右值引用特性代碼和相關的idioms,也可能會遇到和這個相關的bug,一句話,趁早學吧,出來混,老是會碰上的……。

相關文章
相關標籤/搜索