• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            woaidongmao

            文章均收錄自他人博客,但不喜標題前加-[轉(zhuǎn)貼],因其丑陋,見諒!~
            隨筆 - 1469, 文章 - 0, 評論 - 661, 引用 - 0
            數(shù)據(jù)加載中……

            泛型編程-轉(zhuǎn)移構造函數(shù)(Generic Programming: Move Constructor)

             

            http://www.vckbase.com/document/viewdoc/?id=847

            編譯:死貓
            校對:Wang Tianxing

            1 引言

            我相信大家很了解,創(chuàng)建、復制和銷毀臨時對象是C++編譯器最愛的戶內(nèi)運動。不幸的是,這些行為會降低C++程序的性能。確實,臨時對象通常被視為C++程序低效的第一因素[1]

            下面的代碼是正確的:

            vector < string > ReadFile();
            vector < string > vec = ReadFile();

            或者

            string s1, s2, s3;
            //...
            s1 = s2 + s3;

            但是,如果關心效率,則需要限制類似代碼的使用。ReadFile()operator+創(chuàng)建的臨時對象分別被復制然后再廢棄。這是一種浪費!

            為了解決這個問題,需要一些不太優(yōu)雅的約定。例如,可以按照引用傳遞函數(shù)參數(shù):

            void ReadFile(vector < string > & dest);
            vector < string > dest;
            ReadFile(dest);

            這相當令人討厭。更糟的是,運算符沒有這個選擇,所以如果想高效的處理大對象,程序員必須限制創(chuàng)建臨時對象的運算符的使用:

            string s1, s2, s3;
            //...
            s1 = s2;
            s1 += s3;

            這種難纏的手法通常減緩了設計大程序的大團隊的工作效率,這種強加的持續(xù)不斷的煩惱扼殺了編寫代碼的樂趣而且增加了代碼數(shù)量。難道從函數(shù)返回值,使用運算符傳遞臨時對象,這樣做是錯誤的嗎?

            一個正式的基于語言的解決方案的提議已經(jīng)遞交給了標準化委員會[2]Usenet上早已引發(fā)了大討論,本文也因此在其中被反復討論過了。

            本文展示了如何解決C++存在的不必要的復制問題的方法。沒有百分之百讓人滿意地解決方案,但是一個干凈的程度是可以達到的。讓我們一步一步的來創(chuàng)建一個強有力的框架,來幫助我們從程序中消除不需要的臨時對象的復制。這個解決方案不是百分之百透明的,但是它消除了所有的不需要的復制,而且封裝后足以提供一個可靠的替代品,直到多年以后,一個干凈的、基于語言的標準化的實現(xiàn)出現(xiàn)。

            2 臨時對象和轉(zhuǎn)移構造函數(shù)”(Move Constructor)

            在和臨時對象斗爭了一段時間之后,我們意識到在大多數(shù)情況下,完全消除臨時對象是不切實際的。大多數(shù)時候,關鍵是消除臨時對象的復制而不是臨時對象本身。下面詳細的討論一下這個問題。

            大多數(shù)具有昂貴的復制開銷的數(shù)據(jù)結構將它們的數(shù)據(jù)以指針或者句柄的形式儲存。典型的例子包括,字符串(String)類型儲存大小(size)和字符指針(char*),矩陣(Matrix)類型儲存一組整數(shù)維數(shù)和數(shù)據(jù)存儲區(qū)指針(double*),文件(File)類型儲存一個文件句柄(handle)

            如你所見,復制字符串、矩陣或者文件的開銷不是來自于復制實際的數(shù)據(jù)成員,而是來自于指針或者句柄指向的數(shù)據(jù)的復制。

            因此,對于消除復制的目的來說,檢測臨時對象是一個好方法。殘酷點說就是,既然一個對象死定了,我們完全可以趁著它還新鮮,把它用作器官捐獻者。

            順便說一下什么是臨時對象?這里給出一個非正式的定義:

            當且僅當離開一段上下文(context)時在對象上執(zhí)行的僅有的操作是析構函數(shù)時,一個對象被看成是臨時的。這里上下文可能是一個表達式,也可能是一個語句范圍,例如函數(shù)體。

            C++標準沒有定義臨時對象,但是它假定臨時對象是匿名的,例如函數(shù)的返回值。按照我們的更一般化的定義,在函數(shù)中定義的命名的棧分配的變量也是臨時的。稍后為了便于討論我們使用這個一般化的定義。

            考慮這個String類的實現(xiàn)(僅作為示例)

            class String
            {
                   char* data_;
                   size_t length_;
            public:
                   ~String()
                   {
                          delete[] data_;
                   }
                   String(const String& rhs)
                          : data_(new char[rhs.length_]), length_(rhs.length_)
                   {
                          std::copy(rhs.data_, rhs.data_ + length_, data_);
                   }
                   String& operator=(const String&);
                   //...
            };

            這里復制的成本主要由data_的復制組成,也就是分配新的內(nèi)存并復制。如果可以探測到rhs實際上是臨時的就好了。考慮下面的C++偽代碼:

            class String
            {
                   //...同前...
                   String(temporary String& rhs)
                          : data_(rhs.data_), length_(rhs.length_)
                   {
                          //復位源字符串使它可以被銷毀
                          //因為臨時對象的析構函數(shù)仍然要執(zhí)行
                          rhs.data_ =0;
                   }
                   //...
            }

            這個我們虛構的重載構造函數(shù)String(temporary String&)在創(chuàng)建一個String臨時對象(按照前面的定義)時調(diào)用。然后,這個構造函數(shù)執(zhí)行了一個rhs對象轉(zhuǎn)移的構造過程,只是簡單的復制指針而不是復制指針指向的內(nèi)存塊。最后,轉(zhuǎn)移構造函數(shù)復位源指針rhs.data_(恢復為空指針)。使用這個方法,當臨時對象被銷毀時,delete[]會無害的應用在空指針上[譯注:C++保證刪除空指針是安全的]

            一個重要的細節(jié)是轉(zhuǎn)移構造rhs.length_沒有被清0。按照教條主義的觀點,這是不正確的,因為data_==0length_!=0,所以字符串被破壞了。但是,這里有一個很站得住腳的理由,因為rhs的狀態(tài)沒有必要是完整的,只要它可以被安全而正確的銷毀就行了。這是因為會被應用在rhs上唯一一個操作就是析構函數(shù),而不是其他的。所以只要rhs可以被安全的銷毀,而不用去看是否像一個合法的字符串。

            轉(zhuǎn)移構造函數(shù)對于消除不需要的臨時對象復制是一個良好的解決方案。我們只有一個小問題,C++語言中沒有temporary關鍵字。

            還應該注意到臨時對象的探測不會幫助所有的類。有時,所有的數(shù)據(jù)直接存儲在容器中。考慮:

            class FixedMatrix
            {
                   double data_[256][256];
            public:
                   //...操作...
            };

            對這樣一個類,實際上復制成本在于逐字節(jié)的復制sizeof(FixedMatrix)個字節(jié),而探測臨時對象并沒有幫助[譯注:因為數(shù)組不是指針,不能直接交換地址]

            3 過去的解決方案

            不必要的復制是C++社區(qū)長期存在的問題。有兩個努力方向齊頭并進,其一是從編碼和庫編寫的角度,另一個是語言定義和編譯器編寫層面。

            語言/編譯器觀點方面,有返回值優(yōu)化(Return Value Optimization, RVO)RVOC++語言定義所允許[3][譯注:但是不是強制性的,而是實現(xiàn)定義的]。基本上,編譯器假定通過拷貝構造函數(shù)(Copy Constructor)復制返回值。

            確切地說,基于這樣的假定,因此編譯器可以消除不必要的復制。例如,考慮:

            vector< String > ReadFile()
            {
                   vector< String > result;
                   //...填充result...
                   return result;
            }
            vector< String > vec=ReadFile();

            聰明的編譯器可以將vec的地址作為一個隱藏的參數(shù)傳遞給ReadFile而把result創(chuàng)建在那個地址上。所以上面的源代碼生成的代碼看起來像這樣:

            void ReadFile(void* __dest)
            {
                   //使用placement newdest地址創(chuàng)建vector
                   vector< String >& result=
                          *new(__dest) vector< String >;
                   //...填充result...
            }
             
             
            //假設有合適的字節(jié)對齊
            char __buf[sizeof(vector< String >)];
            ReadFile(__buf);
            vector< String >& vec=
                   *reinterpret_cast < vector< String >* >(__buf);

            RVO有不同的風格,但要旨是相同的:編譯器消除了一次拷貝構造函數(shù)的調(diào)用,通過簡單的在最終目的地上構造函數(shù)返回值。

            不幸的是,RVO的實現(xiàn)不像看上那樣容易。考慮ReadFile稍稍修改后的版本:

            vector< String > ReadFile()
            {
                   if (error) return vector< String >();
                   if (anotherError)
                   {
                          vector< String > dumb;
                          dumb.push_back("This file is in error.");
                          return dumb;
                   }
                   vector< String > result;
                   //...填充result...
                   return result;
            }

            ******************************************************

            Wang Tianxing校注:

            這個例子并不是很有說服力。里面的三個對象的作用域互不相交,因此還是比較容易使用 RVO 的。難以運用RVO的是這種情況:

            vector< String > ReadFile()
            {
                   vector< String > dumb;
                   dumb.push_back( "This file is in error." );
             
             
                   vector< String > result;
                   // ... 填充 result ...
             
             
                   return error ? dumb : result;
            }

            ******************************************************

            現(xiàn)在有不止一個局部變量需要被映射到最后的結果上,他們有好幾個。有些是命名的(dumb/result),而另一些是無名的臨時對象。無需多說,面對這樣的局面,大量優(yōu)化器會投降并且服從保守的和缺乏效率的方法。

            即使想寫不導致混淆RVO實現(xiàn)的直線條的代碼,也會因為聽到每個編譯器或者編譯器版本都有自己探測和應用RVO的規(guī)則而失望。一些RVO應用僅僅針對返回無名臨時對象的函數(shù),這是最簡單的RVO形式。最復雜的RVO應用之一是函數(shù)返回值是一個命名的結果,叫做命名返回值優(yōu)化(Named RVONRVO)

            本質(zhì)上,寫程序時要指望可移植的RVO,就要依賴于你的代碼的精確寫法(在很難定義的精確意義下),依賴于月亮的圓缺,依賴于你的鞋的尺碼。

            但是,別忙,還有很多種情況下RVO無法避免臨時對象的拷貝。編譯器時常不能應用RVO,即使它很想。考慮稍稍改變后的 ReadFile() 的調(diào)用:

            vector vec;
            vec=ReadFile();

            這個改變看上去完全沒有惡意,但是卻導致了巨大的差異。現(xiàn)在不再調(diào)用拷貝構造函數(shù)而調(diào)用賦值運算符(assignment operator),這是令一個不同的脫韁野馬。除非編譯器優(yōu)化技巧完全像是在使用魔法,現(xiàn)在真的可以和RVO吻別了:vector<T>::operator=(const vector<T>&)期望一個vector的常量引用,所以ReadFile會返回一個臨時對象,綁定到一個常量引用,復制到vec,然后被廢棄。不必要的臨時對象又來了!

            在編碼方面,一個長期被推薦的技術是COW(按需復制,copy-on-write)[4],這是一個基于引用計數(shù)的技巧。

            COW有幾個優(yōu)點,其中之一是探測和消除了不必要的復制。例如,函數(shù)返回時,返回的對象的引用計數(shù)是1。然后復制的時候,引用計數(shù)增加到2。最后,銷毀臨時對象的時候,引用計數(shù)回到1,引用指向的目的地僅僅是數(shù)據(jù)的所有者。實際上沒有復制動作發(fā)生。

            不幸的是,引用計數(shù)在多線程安全性方面有大量的缺陷,增加自己的開銷和大量隱藏的陷阱[4]COW是如此之笨拙,因此,雖然它有很多優(yōu)點,最近的STL實現(xiàn)都沒有為std::string使用引用計數(shù),盡管實際上std::string的接口有目的設計為支持引用計數(shù)!

            已經(jīng)開發(fā)了幾個實現(xiàn)不可復制對象的辦法,auto_ptr是最精煉的一個。auto_ptr是容易正確使用的,但是不幸的是,剛好也容易不正確的使用。本文的討論的解決方法擴充了定義auto_ptr中使用的技術。

            4 Mojo

            Mojo(聯(lián)合對象轉(zhuǎn)移,Move of Joint Objects)是一項編碼技術,又是一個消除不必要的臨時對象復制的小框架。Mojo通過辨別臨時對象和合法的非臨時的對象而得以工作。

            4.1 傳遞函數(shù)參數(shù)

            Mojo引發(fā)了一個有趣的分析,即函數(shù)參數(shù)傳遞約定的調(diào)查。Mojo之前的一般建議是:

            [規(guī)則1]如果函數(shù)試圖改變參數(shù)(也就是作為副作用),則把參數(shù)作為非常量對象的指針或者引用傳遞。例如:

            void Transmogrify(Widget& toChange);
            void Increment(int* pToBump);

            [規(guī)則2]如果函數(shù)不修改它的參數(shù)而且參數(shù)是基本數(shù)據(jù)類型,則按照值傳遞參數(shù)。例如:

            double Cube(double value);

            [規(guī)則3]否則,參數(shù)是用戶自定義類型(或者模板的類型參數(shù))而且一定不變,則作為常量引用傳遞參數(shù)。例如:

            String& String::operator=(const String& rhs);
            template< class T > vector< T >::push_back(const T&);

            第三條規(guī)則試圖避免意外的大對象的復制。然而,有時第三條規(guī)則強制不必要的復制進行而不是阻止它的發(fā)生。考慮下面的Connect函數(shù):

            void Canonicalize(String& url);
            void ResolveRedirections(String& url);
             
             
            void Connect(const String& url)
            {
                   String finalUrl=url;
                   Canonicalize(finalUrl);
                   ResolveRedirections(finalUrl);
                   //...使用finalUrl...
            }

            Connect函數(shù)獲得一個常量引用的參數(shù),并快速的創(chuàng)建一個副本。然后進一步處理副本。

            這個函數(shù)展示了一個影響效率的常量引用的參數(shù)使用。Connect的函數(shù)聲明暗示了:我不需要一個副本,一個常量引用就足夠了,而函數(shù)體實際上卻創(chuàng)建了一個副本。所以假如現(xiàn)在這樣寫:

            String MakeUrl();
            //...
            Connect(MakeUrl());

            可以預料MakeUrl()會返回一個臨時對象,他將被復制然后銷毀,也就是令人畏懼的不需要的復制模式。對一個優(yōu)化復制的編譯器來說,不得不作非常困難的工作,其一是訪問Connect函數(shù)的定義(這對于分離編譯模塊來說很困難),其二是解析Connect函數(shù)的定義并進一步理解它,其三是改變Connect函數(shù)的行為以使臨時對象和finalUrl融合。

            假如現(xiàn)在將Connect函數(shù)改寫如下:

            void Connect(String url)   //注意按值傳遞
            {
                   Canonicalize(url);
                   ResolveRedirections(url);
                   //... 使用 url ...
            }

            Connect的調(diào)用者的觀點來看,絕對沒有什么區(qū)別:雖然改變了語法接口,但是語義接口仍然是相同的。對編譯器來說,語法的改變使所有事物都發(fā)生了改變。現(xiàn)在編譯器有更多的余地關心url臨時對象了。例如,在上面提到的例子中:

            Connect(MakeUrl());

            編譯器不一定要真的聰明到將MakeUrl返回的臨時對象和Connect函數(shù)需要的常量融合。如果那么做,確實會更加困難。最終,MakeUrl的真正結果會被改變而且在Connect函數(shù)中使用。使用常量引用參數(shù)的版本會使編譯器窒息,阻止它實行任何優(yōu)化,而使用傳值參數(shù)的版本和編譯器順暢的合作。

            這個新版本的不利之處在于,現(xiàn)在調(diào)用Connect也許生成了更多的機器碼。考慮:

            String someUrl=...;
            Connect(someUrl);

            在這種情況下,第一個版本簡單的傳遞someUrl的引用[譯注:從非常量到常量是標準轉(zhuǎn)型]。第二個版本會創(chuàng)建一個someUrl的副本,調(diào)用Connect,然后銷毀那個副本。隨著調(diào)用Connect的靜態(tài)數(shù)量的增長,代碼大小的開銷同時增長。另一方面,例如Connect(MakeUrl())這樣的調(diào)用會引入臨時對象,在第二個版本中又剛好生成更少的代碼。在多數(shù)情況下,大小差異好像不會導致問題產(chǎn)生[譯注:在某些小內(nèi)存應用中則是一個問題,例如嵌入式應用環(huán)境]

            所以我們給出了一套不同的推薦規(guī)則:

            • [規(guī)則1]如果函數(shù)內(nèi)部總是制作參數(shù)的副本,按值傳遞。
            • [規(guī)則2]如果函數(shù)從來不復制參數(shù),按常量引用傳遞。
            • [規(guī)則3]如果函數(shù)有時復制參數(shù),而且關心效率,則按照Mojo協(xié)議。

            現(xiàn)在只留下開發(fā)Mojo協(xié)議了,不管它是什么。

            主要的想法是重載同樣的函數(shù)(例如Connect),目的是辨別臨時的和非臨時的值。后者也稱為左值(lvalue),因為歷史原因,左值因為可以出現(xiàn)在賦值運算符的左邊而得名。

            現(xiàn)在開始重載Connect,第一個想法是定義Connect(const String&)來捕捉常量對象。然而這是錯誤的,因為這個聲明吞吃了所有的String對象,不管是左值(lvalue)或者臨時對象[譯注:前面提到過,非常量可以隱式轉(zhuǎn)型為常量,這是標準轉(zhuǎn)型動作]。所以第一個好主意是不要聲明接受常量引用的參數(shù),因為它像一個黑洞一樣,吞噬所有的對象。

            第二個嘗試是定義Connect(String&)試圖捕獲非常量的左值。這工作良好,特別是常量值和無名的臨時對象不能被這個重載版本接受,這是一個好的起點。現(xiàn)在我們只剩下在常量對象和非常量臨時對象之間作出區(qū)分了。

            為了達到這個目的,我們采取了一種技術,定義兩個替身類型[譯注:原文是type sugar,嘿嘿,如果你愿意,可以叫他類型砂糖,如果你喜歡吃糖的話。]ConstantStringTemporaryString,并且定義了從String對象到這些對象轉(zhuǎn)型運算符:

            class String;
             
             
            //常量String的替身類型
            struct ConstantString
            {
                   const String* obj_;
            };
             
             
            //臨時String的替身類型
            struct TemporaryString : public ConstantString {};
             
             
            class String
            {
            public:
                   //...構造函數(shù),析構函數(shù),運算符,等等......
                   operator ConstantString() const
                   {
                          ConstantString result;
                          result.obj_ = this;
                          return result;
                   }
                   operator TemporaryString()
                   {
                          TemporaryString result;
                          result.obj_ = this;
                          return result;
                   }
            };

            現(xiàn)在定義下面三個重載版本:

            //綁定非常量臨時對象
            void Connect(TemporaryString);
            //綁定所有的常量對象(左值和臨時對象)
            void Connect(ConstantString);
            //綁定非常量左值
            void Connect(String& str)
            {
                   //調(diào)用另一個重載版本
                   Connect(ConstantString(str));
            }

            常量String對象被Connect(ConstantString)吸收。沒有其他綁定可以工作,另兩個僅僅被非常量String對象調(diào)用。

            臨時對象不能調(diào)用Connect(String&)。然而它們可以調(diào)用Connect(TemporaryString)或者Connect(ConstantString),前者必然被選中而不發(fā)生歧義。原因是因為TemporaryStringConstantString派生而來,一個應該注意的詭計。

            考慮一下ConstantStringTemporaryString都是獨立的類型。那么,當要求復制一個臨時對象時,編譯器將同等的對待operator TemporaryY()/Y(TemporarY)或者operator ConstantY() const/Y(ConstantY)

            為什么是同等的?因為就選擇成員函數(shù)來說,非常量到常量轉(zhuǎn)型是無摩擦的

            因而,需要告訴編譯器更多的選擇第一個而不是第二個。那就是繼承在這里的作用。現(xiàn)在編譯器說:好吧,我猜我要經(jīng)過ConstantString或者TemporaryString...,但是等等,派生類TemporaryString是更好的匹配!

            這里的規(guī)則是從重載候選中選擇函數(shù)時,匹配的派生類被視作比匹配的基類更好。

            [譯注]

            我對上述代碼稍作修改,從std::string派生了String,并在此基礎上按照Mojo的方式修改,結果在gcc3.2編譯器下的確如作者指出的行為一般無二。這條重載的決議規(guī)則很少在C++書籍中提到,Wang Tianxing從煙波浩淼的標準文本中找出了這條規(guī)則:

            13.3.3.2 Ranking implicit conversion sequences [over.rank]
             
             
            4 [...]
            -- If class B is derived directly or indirectly
               from class A and class C is derived directly
               or indirectly from B,
              [...]
            -- binding of an expression of type C to a
                 object of type B is better than binding
                 an expression of type C to a object
                 of object A,

            上面這些標準中的條款,是從隱式轉(zhuǎn)型的轉(zhuǎn)換等級中節(jié)選出來的,大致的意思是說,如果C繼承B,而B繼承A,那么類型為C的表達式綁定到B的對象比到A的對象更好,這是上面敘述的技術的標準依據(jù)。此外,類似的引用和指針的綁定也適用于此規(guī)則,這里省略了這些條款。

            最后一個有趣的花樣是,繼承不需要必須是public的。存取規(guī)則和重載規(guī)則是不沖突的。

            讓我們看看Connect如何工作的例子:

            String s1("http://moderncppdesign.com");
            // 調(diào)用Connect(String&)
            Connect(s1);
            // 調(diào)用operator TemporaryString()
            // 接下來調(diào)用Connect(TemporaryString)
            Conncet(String("http://moderncppdesign.com"));
            const String s4("http://moderncppdesign.com");
            // 調(diào)用operator ConstantString() const
            // 接下來調(diào)用Connect(ConstantString)
            Connect(s4);

            如你所見,我們達到了期望的主要目標:在臨時對象和所有其他對象之間制造了差別。這就是Mojo的要旨。

            還有一些不太顯眼的問題,大多數(shù)我們要一一解決。

            首先是減少代碼重復:Connect(String&)Connect(ConstantString)基本上作相同的事情。上面的代碼通過第一個重載函數(shù)調(diào)用第二個重載函數(shù)解決了這個問題。

            讓我們面對第二個問題,為每個需要mojo的類型寫兩個小類聽上去不是很吸引人,所以讓我們開始制作一些更具一般性的東西更便于使用。我們定義了一個mojo名字空間,并放入兩個泛型的ConstantTemporary類:

            namespace mojo
            {
                   template < class T >
                   class constant
                   {
                          const T* data_;
                   public:
                          explicit constant(const T& obj) : data_(&obj)
                          {
                          }
                          const T& get() const
                          {
                                return *data_;
                          }
                   };
                   
                   template < class T >
                   class temporary : private constant< T >
                   {
                   public:
                          explicit temporary(T& obj) : contant< T >( obj)
                          {
                          }
                          T& get() const
                          {
                                return const_cast< T& >(constant< T >::get());
                          }
                   };
            }

            讓我們再定義一個基類mojo::enabled,它包括了兩個運算符:

            template < class T > struct enabled //mojo名字空間中
            {
                   operator temporary< T >()
                   {
                          return temporary< T >(static_cast< T& >(*this));
                   }
                   operator constant< T >() const
                   {
                          return constant< T >(static_cast< const T& >(*this));
                   }
            protected:
                   enabled() {} //只能被派生
                   ~enabled() {} //只能被派生
            };

            使用這個腳手架,將一個類“mojo的任務可以想象會變得更簡單:

            class String : public mojo::enabled< String >
            {
                   //...構造函數(shù),析構函數(shù),運算符,等等...
            public:
                   String(mojo::temporary< String > tmp)
                   {
                          String& rhs = tmp.get();
                          //...執(zhí)行rhs*this的析構性復制...
                   }
            };

            這就是傳遞函數(shù)參數(shù)的Mojo協(xié)議。

            通常,一切工作良好,你得到了一個好的設計品。不錯,那些意外的情況都控制在一個很小的范圍內(nèi),這使他們更有價值。

            Mojo設計我們可以很容易檢測到一個類是否支持Mojo。只需要簡單的寫:

            namespace mojo
            {
                   template < class T >
                   struct traits
                   {
                          enum
                          {
                                enabled = Loki::SuperSubclassStrict< enabled< T >, T >::value
                          };
                   };
            };

            Loki提供了探測一個類型是否從另一個類派生的機制。[5]

            現(xiàn)在可以發(fā)現(xiàn)一個任意的類型X是按照Mojo協(xié)議設計的,只要通過mojo::traits<X>::enabled即可確定。這個檢測機制對泛型編程是很重要的,很快我們就會看到它的作用。

            4.2 函數(shù)返回值優(yōu)化

            現(xiàn)在我們可以正確的傳遞參數(shù),讓我們看看如何將Mojo擴展到函數(shù)返回值優(yōu)化。這次的目的又是具有可移植性的效率改善,即100%的消除不需要的復制而不依賴于特定的返回值優(yōu)化(RVO)實現(xiàn)。

            讓我們先看看通常的建議怎么說。出于好意,一些作者也推薦返回值的使用規(guī)則[7]

            • [規(guī)則4]當函數(shù)返回用戶定義的對象的值的時候,返回一個常量值。例如:
            const String operator+(const String& lhs,const String& rhs);

            規(guī)則4的潛臺詞是使用戶定義的運算符更加接近于內(nèi)建的運算符可以禁止錯誤的表達式的功能,就好像想是if (s1+s2==s3)的時候筆誤成了if (s1+s2=s3)。如果operator+返回一個常量值,這個特定的BUG將會在編譯期間被檢測到[譯注:返回內(nèi)建數(shù)據(jù)類型的值隱含地總是常量的,而用戶定義類型則需要顯式的用常量限定符指出]。然而,其他的作者[6]推薦不要返回常量值。

            冷靜的看,任何返回值都是短暫的,它是剛剛被創(chuàng)建就要很快消失的短命鬼。那么,為什么要強迫運算符的使用者獲得一個常量值呢?從這個觀點看,常量的臨時對象看上去就象是自相矛盾的,既是不變的,又是臨時的。從實踐的觀點看,常量對象強迫復制。

            現(xiàn)在假定我們同意,如果效率是重要的,最好是避免返回值是常量,那么我們?nèi)绾问咕幾g器確信將函數(shù)的結果轉(zhuǎn)移到目的地,而不是復制他呢?

            當復制一個類型為T的對象時,拷貝構造函數(shù)被調(diào)用。按照下面的設置,我們剛好可以提供這樣一個拷貝構造函數(shù)實現(xiàn)這個目標。

            class String : public mojo :: enabled < string >
            {
            //...
            public:
              String( String& );
              String( mojo :: temporary < String > );
              String( mojo :: constant < String > );
            };

            這是一個很好的設計,除了一個小細節(jié)--它不能工作。

            因為拷貝構造函數(shù)和其他的函數(shù)不完全相同,特別是,對一個類型X來說,在需要X(const X&)的地方定義X(X&),下面的代碼將無法工作:

            void FunctionTakingX(const X&);
            FunctionTakingX(X());  // 錯誤!不能發(fā)現(xiàn)X(const X&)

            [譯注]

            Wang Tianxinggcc3.2, bcc5.5.1, icl7.0環(huán)境下測試結果表明都不會發(fā)生錯誤,并進而查閱了標準,發(fā)現(xiàn)Andrei是正確的,如果一定說要有什么錯誤的話,他沒有指出這是實現(xiàn)定義的。

            8.5.3 References
             
             
            5 [...]
             
             
            — If the initializer expression is an rvalue, with T2 a class type,
            and “cv1 T1” is reference-compatible with “cv2 T2,” the reference
            is bound in one of the following ways (the choice is implementation-
            defined):
             
             
            — The reference is bound to the object represented by the rvalue
            (see 3.10) or to a sub-object within that object.
             
             
            — A temporary of type “cv1 T2” [sic] is created, and a
            constructor is called to copy the entire rvalue object into the
            temporary. The reference is bound to the temporary or to a
            sub-object within the temporary.93)
             
             
            The constructor that would be used to make the copy shall be
            callable whether or not the copy is actually done.
             
             
            93) Clearly, if the reference initialization being processed is one
            for the first argument of a copy constructor call, an implementation
            must eventually choose the first alternative (binding without
            copying) to avoid infinite recursion.

            我引用了這段標準文本,有興趣的讀者可以自行研究它的含義。

            這嚴重的限制了X,所以我們被迫實現(xiàn)String(const String&)構造函數(shù)。現(xiàn)在如果你允許我引用本文的話,在前面我曾經(jīng)說過:所以第一個好主意是不要聲明一個函數(shù)接受常量引用,因為它像一個黑洞一樣吞噬所有的對象。

            魚與熊掌不可兼得,不是嗎?

            很清楚,拷貝構造函數(shù)需要特別的處理。這里的想法是創(chuàng)建一個新的類型fnresult,那就是為String對象提供一個轉(zhuǎn)移器(mover)”。下面是需要執(zhí)行的步驟:

            1. 前面返回類型為T的值的函數(shù)現(xiàn)在將返回fnresult<T>。為了使這個變化對對調(diào)用者透明,fnresult必須可以被隱式的轉(zhuǎn)型為T
            2. 然后為fnresult建立轉(zhuǎn)移語義:無論何時一個fnresult<T>對象被復制,里面包含的T被轉(zhuǎn)移。
            3. 類似運算符的常量性和臨時性,在mojo::enabled類中為fnresult提供一個轉(zhuǎn)型運算符。
            4. 一個mojo化的類(如前例中的String)定義了一個構造函數(shù)String( mojo :: fnresult < String > )完成轉(zhuǎn)移。

            這個fnresult的定義看起來就像:

            namespace mojo
            {
              template < class T >
              class fnresult : public T
              {
              public:
                fnresult ( const fnresult& rhs )
                  :  T ( temporary < T > ( const_cast < fnresult& > ( rhs ) ) )
                {
                }
                explicit fnresult ( T& rhs ) : T ( temporary < T > ( rhs ) )
                {
                }
              };
            }

            因為fnresult<T>T繼承而來,第一步值得注意,即fnresult<T>轉(zhuǎn)型為T,然后第二個值得注意的就是復制fnresult<T>對象的時候,隱含著它的T子對象(subobject)強制轉(zhuǎn)型為temporary<T>

            正如前面提到的,我們增加一個轉(zhuǎn)型允許返回一個fnresult,最后的版本看起來是這樣的:

            template < class T > struct enabled
            {
              operator temporary < T > ( )
              {
                return temporary < T > ( static_cast < T& > ( *this ) );
              }
              operator constant < T > ( ) const
              {
                return constant < T > ( static_cast < const T& > ( *this ) );
              }
              operator fnresult < T > ( )
              {
                return fnresult < T > ( static_cast < T& > ( *this ) );
              }
              protected:
                enabled ( ) { } // intended to be derived from
                ~enabled ( ) { } // intended to be derived from
            };

            最后是String的定義:

            class String : public mojo :: enabled < String >
            {
              //...
            public:
              // COPY rhs
              String ( const String& rhs ); 
              // MOVE tmp.get() into *this
              String ( mojo :: temporary < String > tmp ); 
              // MOVE res into *this
              String ( mojo :: fnresult < String > res ); 
            };

            現(xiàn)在考慮下面的函數(shù):

            mojo :: fnresult < String > MakeString()
            {
              String result;
             //?..
              return result;
            }
            //...
            String dest(MakeString());

            MakeStringreturn語句和dest的定義之間的路徑是:

            result -> String :: operator fnresult < String > () -> fnresult < String > (const fnresult < String >& ) -> String :: String ( fnresult < String > )

            使用RVO的編譯器可以消除調(diào)用鏈中fnresult<String>(const fnresult<String>&)的調(diào)用。然而,更重要的是沒有函數(shù)執(zhí)行真正的復制,它們都被定義為結果的實際內(nèi)容平滑的轉(zhuǎn)移到dest。也就是說沒有涉及內(nèi)存分配和復制。

            現(xiàn)在,正如所見,有兩個,最多三個轉(zhuǎn)移操作。當然,在一定條件和一定類型的情況下,一次復制比三次轉(zhuǎn)移可能更好。還有一個重要的區(qū)別,復制也許會失敗(拋出異常),而轉(zhuǎn)移永遠不會失敗。

            5 擴展

            好的,我們使Mojo工作了,而且對于單獨的類相當好。現(xiàn)在怎樣將Mojo擴展到組合對象,它們也許包含大量其他的對象,而且他們中的一些已經(jīng)是mojo化的。

            這個任務就是將轉(zhuǎn)移構造函數(shù)從類傳遞到成員。考慮下面的例子,內(nèi)嵌類String在類Widget中:

            class Widget : public mojo::enabled < Widget >
            {
              String name_;
            public:
              Widget(mojo::temporary< Widget > src) // source is a temporary
                : name_(mojo::as_temporary(src.get().name_))
              {
                Widget& rhs = src.get();
                //... use rhs to perform a destructive copy ... 
              }
              Widget(mojo::constant< Widget > src) // source is a const
                : name_(src.get().name_) // 譯注:這里原文name_(src.name_)顯然有誤
              {
                Widget& rhs = src;
                //... use rhs to perform a destructive copy ... 
              }
            };

            在轉(zhuǎn)移構造函數(shù)中的name_的初始化使用了一個重要的Mojo輔助函數(shù):

            namespace mojo
            {
              template < class T >
              struct traits
              {
                enum { enabled = 
                  Loki::SuperSubclassStrict< enabled< T >, T >::value };
                typedef typename 
                   Loki::Select< enabled,temporary< T >,T& >::Result temporary;
              };
              template < class T >
              inline typename traits< T >::temporary as_temporary(T& src)
              {
                typedef typename traits< T >::temporary temp;
                return temp(src);
              }
            }

            as_temporary做的所有事情就是根據(jù)一個左值創(chuàng)建一個臨時對象。使用這個方法,類成員的轉(zhuǎn)移構造函數(shù)被目標對象所調(diào)用。

            如果Stringmojo化的,Widget得到他的優(yōu)點;如果不是,一個直接的復制被執(zhí)行。換句話說,如果Stringmojo::enabled<String>的一個派生類,那么as_temporary返回一個mojo::temporary<String>。否則,as_temproary(String& src)是一個簡單的函數(shù),帶一個String&的參數(shù)并返回同樣的String&

            6 應用:auto_ptr的親戚和mojo化的容器

            考慮一個mojo_ptr類,它通過使拷貝構造函數(shù)私有而禁止它們:

            class mojo_ptr : public mojo::enable< mojo_ptr >
            {
              mojo_ptr(const mojo_ptr&); // const sources are NOT accepted
            public:
              // source is a temporary
              mojo_ptr(mojo::temporary< mojo_ptr > src) 
              {
                mojo_ptr& rhs = src.get();
                //... use rhs to perform a destructive copy ... 
              }
              // source is a function's result
              mojo_ptr(mojo::fnresult< mojo_ptr > src) 
              {
                mojo_ptr& rhs = src.get();
                //... use rhs to perform a destructive copy ... 
              } 
              //..
            };

            這個類有一個有趣的行為。你不能復制這個類的常量對象。你也不能復制這個類的左值。但是你可以復制這個類的臨時對象(使用轉(zhuǎn)移語義),而且你可以顯式的移動一個對象到另外的對象:

            mojo_ptr ptr1;
            mojo_ptr ptr2 = mojo::as_temporary(ptr1);

            這本身并沒有什么大不了的,如果 auto_ptr 里讓 auto_ptr(auto_ptr&)私有,也可以做到這一點。有趣的地方不是mojo_ptr本身,而是如何使用as_temporary。你可以建立高效的容器,儲存經(jīng)典的類型、一般的mojo化的類型以及和mojo_ptr類似的類型。所有這樣的一個容器當他需要轉(zhuǎn)移元素時,必須使用as_temporary。對于經(jīng)典類型,as_temporary是一個什么都不做的等效函數(shù),對于mojo_ptras_temporary是一個提供平滑轉(zhuǎn)移機制的函數(shù)書。move()以及uninitialized_move()的函數(shù)模板(參見所附代碼,譯注:代碼請到原版鏈接處尋找)也唾手可得。

            使用標準術語,mojo_ptr既不是可以復制的,也不是可以賦值的。然而,mojo_ptr可以看作是一種新類型,叫做可轉(zhuǎn)移的。這是一個重要的新的分類,也許可以用于鎖(lock)、文件(file)和其他的不可復制的句柄(handle)

            如果你曾經(jīng)希望一個擁有元素的類似于 vector< auto_ptr<Widget> > 的容器,而且有安全、清楚的語義,現(xiàn)在你得到了,而且還有其他功能。另外,當包含一個拷貝昂貴的類型時,如vector< vector<string> >mojo化的vector“更能適應元素個數(shù)增減的需要

            7 結論

            mojo是一種技術,也是一個緊湊的小框架,用于消除不必要的臨時對象的復制。mojo的工作方式是檢測臨時對象并且通過函數(shù)重載操縱他們而不是簡單的作為左值。這樣做的結果是,獲得臨時對象的函數(shù)執(zhí)行一個破壞性的復制,只要確信其他代碼不再使用這個臨時對象即可。

            如果客戶代碼按照一套簡單的規(guī)則傳遞函數(shù)參數(shù)和返回值,可以應用mojo

            mojo定義了一個單獨的機制來消除函數(shù)返回時的復制。

            額外的機制和類型轉(zhuǎn)換使mojo對于客戶代碼不是100%的透明,然而對于基于庫的解決方案來說集成度是相當好的。說得好聽一點,mojo將作為一個健壯的替代品,直到一個更健壯的、基于語言特性的被標準化并實現(xiàn)。

            8 致謝

            原文的致謝略,譯文得到了Wang Tianxing的熱情幫助,除了幫助我審核了若干技術細節(jié)之外,還指出了不少打字錯誤,以及若干英語中的諺語。

            9 參考文獻

            [1] Dov Bulka and David Mayhew. Efficient C++: Performance Programming Techniques, (Addison-Wesley, 1999).

            [2] Howard E. Hinnant, Peter Dimov, and Dave Abrahams. "A Proposal to Add Move Semantics Support to the C++ Language," ISO/IEC JTC1/SC22/WG21 — C++, document number N1377=02-0035, September 2002, <http://anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2002/n1377.htm>.

            [3] "Programming Languages — C++," International Standard ISO/IEC 14882, Section 12.2.

            [4] Herb Sutter. More Exceptional C++ (Addison-Wesley, 2002).

            [5] Andrei Alexandrescu. Modern C++ Design (Addison-Wesley, 2001).

            [6] John Lakos. Large-Scale C++ Software Design (Addison-Wesley, 1996), Section 9.1.9.

            [7] Herb Sutter. Exceptional C++ (Addison-Wesley, 2000).

            作者簡介

            Andrei Alexandrescu是一位華盛頓大學西雅圖分校的博士生,廣受贊譽的《Modern C++ Design》(中譯本現(xiàn)代C++設計正在譯制中)一書的作者。可以通過電子郵件andrei@metalanguage.com聯(lián)系。Andrei還是一個C++課程的有號召力的講師。

            譯者的話

            作為第一次編譯技術文章,而且選擇的是C++中自己相對比較陌生的主題,并且本文講述的內(nèi)容是具有前瞻性的,而不是見諸于現(xiàn)有資料和文獻的重新整理。因此在翻譯過程中,有些細節(jié)譯者本人也沒有完全理解,因此難免出現(xiàn)不少差錯,歡迎大家來到newsfanC++新聞組討論

             

            posted on 2009-12-02 17:14 肥仔 閱讀(1612) 評論(2)  編輯 收藏 引用 所屬分類: C++ 基礎

            評論

            # re: 泛型編程-轉(zhuǎn)移構造函數(shù)(Generic Programming: Move Constructor)  回復  更多評論   

            學習了
            2012-02-16 15:23 | 溪流

            # re: 泛型編程-轉(zhuǎn)移構造函數(shù)(Generic Programming: Move Constructor)  回復  更多評論   

            借地問大家一個問題:
            const r-value 存在嗎?怎么構造?
            2012-02-16 16:18 | 溪流
            亚洲国产精品久久电影欧美| 久久午夜羞羞影院免费观看| 国产精品久久久久AV福利动漫 | 日韩人妻无码一区二区三区久久| 精品无码久久久久久久久久| 国产精品美女久久久久久2018| 精品久久久无码人妻中文字幕| 久久久免费观成人影院| 国产精品99久久久久久宅男 | 日本精品久久久久中文字幕| 97久久精品无码一区二区| 人妻精品久久久久中文字幕69 | 国产精品xxxx国产喷水亚洲国产精品无码久久一区| 国产精品成人久久久| 热久久最新网站获取| 色婷婷噜噜久久国产精品12p | 无码国内精品久久人妻麻豆按摩 | 蜜桃麻豆www久久国产精品| 久久精品国产只有精品66| 久久夜色精品国产亚洲av| 国产精品美女久久久免费| 久久乐国产精品亚洲综合| 久久久午夜精品福利内容| 精品国产青草久久久久福利| 日韩人妻无码精品久久久不卡| 久久综合亚洲欧美成人| 国产人久久人人人人爽| 久久er国产精品免费观看2| 嫩草影院久久国产精品| 国产 亚洲 欧美 另类 久久| 国产成人久久精品麻豆一区| 久久久久亚洲精品天堂久久久久久 | 伊人久久大香线蕉亚洲| 国产精品久久久久…| 久久九九免费高清视频| 久久精品国产乱子伦| 丁香狠狠色婷婷久久综合| 久久精品亚洲精品国产欧美| 国内精品久久久久影院薰衣草| 97久久国产亚洲精品超碰热| 国产成人无码精品久久久久免费 |