[CUJ]泛型程式設計--轉移建構函式 (轉)

worldblog發表於2007-12-14
[CUJ]泛型程式設計--轉移建構函式 (轉)[@more@]

主題:泛型-轉移構造(Generic Programming: Move Constructor)

作者:Andrei Alexandrescu

編譯:to:smao@21cn.com">死貓

校對:Wang Tianxing

原文:

http://www.cuj.com/experts/2102/alexandr.htm

摘要:

本文介紹了使用模板技術消除C++中的不必要的臨時複製的方法。除此之外,本文中使用了不少平時很少注意到的技術,即使你對消除臨時物件的複製不感興趣,本文也值得一讀。

關鍵字:臨時物件 模板


1 引言

我相信大家很瞭解,建立、複製和銷燬臨時物件是C++最愛的戶內運動。不幸的是,這些行為會降低C++的。確實,臨時物件通常被視為C++程式低效的第一因素[1]。

下面的程式碼是正確的:

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


或者

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


但是,如果關心,則需要限制類似程式碼的使用。ReadFile()和operator+建立的臨時物件分別被複制然後再廢棄。這是一種浪費!

為了解決這個問題,需要一些不太優雅的約定。例如,可以按照引用傳遞函式引數:

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


這相當令人討厭。更糟的是,運算子沒有這個選擇,所以如果想高效的處理大物件,程式設計師必須限制建立臨時物件的運算子的使用:

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


這種難纏的手法通常減緩了設計大程式的大團隊的工作效率,這種強加的持續不斷的煩惱扼殺了編寫程式碼的樂趣而且增加了程式碼數量。能夠從函式返回"值", 能夠使用運算子, 還能把臨時變數傳來遞去, 並且在這種自由地建立/複製/銷燬的過程裡沒有時間被浪費 -- 要是能這樣兩全其美該多好呀!

一個正式的基於語言的解決方案的提議已經遞交給了標準化委員會[2]。Us上早已引發了大討論,本文也因此在其中被反覆討論過了。

本文展示瞭如何解決C++存在的不必要的複製問題的方法。沒有百分之百讓人滿意地解決方案,但是一個乾淨的程度是可以達到的。讓我們一步一步的來建立一個強有力的,來幫助我們從程式中消除不需要的臨時物件的複製。這個解決方案不是百分之百透明的,但是它消除了所有的不需要的複製,而且封裝後足以提供一個可靠的替代品,直到多年以後,一個乾淨的、基於語言的標準化的實現出現。

2 臨時物件和“轉移建構函式”(Move Constructor)

在和臨時物件鬥爭了一段時間之後,我們意識到在大多數情況下,完全消除臨時物件是不切實際的。大多數時候,關鍵是消除臨時物件的複製而不是臨時物件本身。下面詳細的討論一下這個問題。

大多數具有昂貴的複製開銷的資料結構將它們的資料以指標或者控制程式碼的形式儲存。典型的例子包括,字串(String)型別儲存大小(size)和字元指標(char*),矩陣(Matrix)型別儲存一組整數維數和資料區指標(double*),(File)型別儲存一個檔案控制程式碼(handle)。

如你所見,複製字串、矩陣或者檔案的開銷不是來自於複製實際的資料成員,而是來自於指標或者控制程式碼指向的資料的複製。

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

順便說一下什麼是臨時物件?這裡給出一個非正式的定義:

當且僅當離開一段上下文(context)時在物件上的僅有的操作是解構函式時,一個物件被看成是臨時的。這裡上下文可能是一個,也可能是一個語句範圍,例如函式體。

C++標準沒有定義臨時物件,但是它假定臨時物件是匿名的,例如函式的返回值。按照我們的更一般化的定義,在函式中定義的命名的棧分配的變數也是臨時的。稍後為了便於討論我們使用這個一般化的定義。

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

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_的複製組成,也就是分配新的並複製。如果可以探測到rhs實際上是臨時的就好了。考慮下面的C++虛擬碼:

class String { //...同前... String(temporary String& rhs) : data_(rhs.data_), length_(rhs.length_) { //復位源字串使它可以被銷燬 //因為臨時物件的解構函式仍然要執行 rhs.data_ =0; } //... }


這個我們虛構的過載建構函式String(temporary String&)在建立一個String臨時物件(按照前面的定義)時。然後,這個建構函式執行了一個rhs物件轉移的構造過程,只是簡單的複製指標而不是複製指標指向的記憶體塊。最後,“轉移建構函式”復位源指標rhs.data_(恢復為空指標)。使用這個方法,當臨時物件被銷燬時,delete[]會無害的應用在空指標上[譯註:C++保證刪除空指標是的]

一個重要的細節是“轉移構造”後rhs.length_沒有被清0。按照教條主義的觀點,這是不正確的,因為data_==0而length_!=0,所以字串被破壞了。但是,這裡有一個很站得住腳的理由,因為rhs的狀態沒有必要是完整的,只要它可以被安全而正確的銷燬就行了。這是因為會被應用在rhs上唯一一個操作就是解構函式,而不是其他的。所以只要rhs可以被安全的銷燬,而不用去看是否像一個合法的字串。

“轉移建構函式”對於消除不需要的臨時物件複製是一個良好的解決方案。我們只有一個小問題,C++語言中沒有temporary關鍵字。

還應該注意到臨時物件的探測不會幫助所有的類。有時,所有的資料直接儲存在容器中。考慮:

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


對這樣一個類,實際上覆製成本在於逐位元組的複製sizeof(FixedMatrix)個位元組,而探測臨時物件並沒有幫助[譯註:因為陣列不是指標,不能直接地址]。

3 過去的解決方案

不必要的複製是C++社群長期存在的問題。有兩個努力方向齊頭並進,其一是從編碼和庫編寫的角度,另一個是語言定義和編譯器編寫層面。

語言/編譯器觀點方面,有返回值(Return Value Optimization, RVO)。RVO被C++語言定義所允許[3][譯註:但是不是強制性的,而是實現定義的]。基本上,編譯器假定透過複製建構函式(Copy Constructor)複製返回值。

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

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


聰明的編譯器可以將vec的地址作為一個隱藏的引數傳遞給ReadFile而把result建立在那個地址上。所以上面的生成的程式碼看起來像這樣:

void ReadFile(void* __dest) { //使用placement new在dest地址建立vector vector< String >& result= *new(__dest) vector< String >; //...填充result... } //假設有合適的位元組對齊 char __buf[sizeof(vector< String >)]; ReadFile(__buf); vector< String >& vec= *reinterpret_cast < vector< String >* >(__buf);


RVO有不同的風格,但要旨是相同的:編譯器消除了一次複製建構函式的呼叫,透過簡單的在最終目的地上建構函式返回值。

不幸的是,RVO的實現不像看上那樣容易。考慮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; }


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

現在有不止一個區域性變數需要被對映到最後的結果上,他們有好幾個。有些是命名的(dumb/result),而另一些是無名的臨時物件。無需多說,面對這樣的局面,大量最佳化器會投降並且服從保守的和缺乏效率的方法。

即使想寫不導致混淆RVO實現的“直線條”的程式碼,也會因為聽到每個編譯器或者編譯器版本都有自己探測和應用RVO的規則而失望。一些RVO應用僅僅針對返回無名臨時物件的函式,這是最簡單的RVO形式。最複雜的RVO應用之一是函式返回值是一個命名的結果,叫做命名返回值最佳化(Named RVO或NRVO)。

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

但是,別忙,還有很多種情況下RVO無法避免臨時物件的複製。編譯器時常不能應用RVO,即使它很想。考慮稍稍改變後的 ReadFile() 的呼叫:

vector vec; vec=ReadFile();

這個改變看上去完全沒有惡意,但是卻導致了巨大的差異。現在不再呼叫複製建構函式而呼叫賦值運算子(assignment operator),這是令一個不同的脫韁野馬。除非編譯器最佳化技巧完全像是在使用魔法,現在真的可以和RVO吻別了:vector::operator=(const vector&)期望一個vector的常量引用,所以ReadFile會返回一個臨時物件,繫結到一個常量引用,複製到vec,然後被廢棄。不必要的臨時物件又來了!

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

COW有幾個優點,其中之一是探測和消除了不必要的複製。例如,函式返回時,返回的物件的引用計數是1。然後複製的時候,引用計數增加到2。最後,銷燬臨時物件的時候,引用計數回到1,引用指向的目的地僅僅是資料的所有者。實際上沒有複製動作發生。

不幸的是,引用計數在多執行緒安全性方面有大量的缺陷,增加自己的開銷和大量隱藏的陷阱[4]。COW是如此之笨拙,因此,雖然它有很多優點,最近的STL實現都沒有為std::string使用引用計數,儘管實際上std::string的介面有目的設計為支援引用計數!

已經開發了幾個實現“不可複製”物件的辦法,auto_ptr是最精煉的一個。auto_ptr是容易正確使用的,但是不幸的是,剛好也容易不正確的使用。本文的討論的解決方法擴充了定義auto_ptr中使用的技術。

4 Mojo

Mojo(聯合物件轉移,Move of Joint s)是一項編碼技術,又是一個消除不必要的臨時物件複製的小框架。Mojo透過辨別臨時物件和合法的“非臨時”的物件而得以工作。

4.1 傳遞函式引數

Mojo引發了一個有趣的分析,即函式引數傳遞約定的調查。Mojo之前的一般建議是:

[規則1]如果函式試圖改變引數(也就是作為副作用),則把引數作為非常量物件的指標或者引用傳遞。例如:

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


[規則2]如果函式不修改它的引數而且引數是基本資料型別,則按照值傳遞引數。例如:

double Cube(double value);


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

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


第三條規則試圖避免意外的大物件的複製。然而,有時第三條規則強制不必要的複製進行而不是阻止它的發生。考慮下面的Connect函式:

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


Connect函式獲得一個常量引用的引數,並的建立一個副本。然後進一步處理副本。

這個函式展示了一個影響效率的常量引用的引數使用。Connect的函式宣告暗示了:“我不需要一個副本,一個常量引用就足夠了”,而函式體實際上卻建立了一個副本。所以假如現在這樣寫:

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


可以預料MakeUrl()會返回一個臨時物件,他將被複制然後銷燬,也就是令人畏懼的不需要的複製。對一個最佳化複製的編譯器來說,不得不作非常困難的工作,其一是訪問Connect函式的定義(這對於分離編譯模組來說很困難),其二是解析Connect函式的定義並進一步理解它,其三是改變Connect函式的行為以使臨時物件和finalUrl融合。

假如現在將Connect函式改寫如下:

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


從Connect的呼叫者的觀點來看,絕對沒有什麼區別:雖然改變了語法介面,但是語義介面仍然是相同的。對編譯器來說,語法的改變使所有事物都發生了改變。現在編譯器有更多的餘地關心url臨時物件了。例如,在上面提到的例子中:

Connect(MakeUrl());


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

這個新版本的不利之處在於,現在呼叫Connect也許生成了更多的機器碼。考慮:

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


在這種情況下,第一個版本簡單的傳遞someUrl的引用[譯註:從非常量到常量是標準轉型]。第二個版本會建立一個someUrl的副本,呼叫Connect,然後銷燬那個副本。隨著呼叫Connect的靜態數量的增長,程式碼大小的開銷同時增長。另一方面,例如Connect(MakeUrl())這樣的呼叫會引入臨時物件,在第二個版本中又剛好生成更少的程式碼。在多數情況下,大小差異好像不會導致問題產生[譯註:在某些小記憶體應用中則是一個問題,例如嵌入式應用環境]

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

  • [規則1]如果函式內部總是製作引數的副本,按值傳遞。
  • [規則2]如果函式從來不復制引數,按常量引用傳遞。
  • [規則3]如果函式有時複製引數,而且關心效率,則按照Mojo。

現在只留下開發Mojo協議了,不管它是什麼。

主要的想法是過載同樣的函式(例如Connect),目的是辨別臨時的和非臨時的值。後者也稱為左值(lvalue),因為歷史原因,左值因為可以出現在賦值運算子的左邊而得名。

現在開始過載Connect,第一個想法是定義Connect(const String&)來捕捉常量物件。然而這是錯誤的,因為這個宣告“吞吃”了所有的String物件,不管是左值(lvalue)或者臨時物件[譯註:前面提到過,非常量可以隱式轉型為常量,這是標準轉型動作]。所以第一個好主意是不要宣告接受常量引用的引數,因為它像一個黑洞一樣,吞噬所有的物件。

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

為了達到這個目的,我們採取了一種技術,定義兩個替身型別[譯註:原文是type sugar,嘿嘿,如果你願意,可以叫他型別砂糖,如果你喜歡吃糖的話。]ConstantString和TemporaryString,並且定義了從String物件到這些物件轉型運算子:

class String; //常量String的替身型別 struct ConstantString { const String* obj_; }; //臨時String的替身型別 struct TemporaryString : public ConstantString {}; class String { public: //...建構函式,解構函式,運算子,等等...... operator ConstantString() const { ConstantString result; result.obj_ = this; return result; } operator TemporaryString() { TemporaryString result; result.obj_ = this; return result; } };


現在定義下面三個過載版本:

//繫結非常量臨時物件 void Connect(TemporaryString); //繫結所有的常量物件(左值和臨時物件) void Connect(ConstantString); //繫結非常量左值 void Connect(String& str) { //呼叫另一個過載版本 Connect(ConstantString(str)); }


常量String物件被Connect(ConstantString)吸收。沒有其他繫結可以工作,另兩個僅僅被非常量String物件呼叫。

臨時物件不能呼叫Connect(String&)。然而它們可以呼叫Connect(TemporaryString)或者Connect(ConstantString),前者必然被選中而不發生歧義。原因是因為TemporaryString從ConstantString派生而來,一個應該注意的詭計。

考慮一下ConstantString和TemporaryString都是獨立的型別。那麼,當要求複製一個臨時物件時,編譯器將同等的對待operator TemporaryY()/Y(TemporarY)或者operator ConstantY() const/Y(ConstantY)。

為什麼是同等的?因為就選擇成員函式來說,非常量到常量轉型是“無摩擦的”。

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

這裡的規則是從過載候選中選擇函式時,匹配的派生類被視作比匹配的基類更好。

[譯註]

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

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,


上面這些標準中的條款,是從隱式轉型的轉換等級中節選出來的,大致的意思是說,如果C繼承B,而B繼承A,那麼型別為C的表示式繫結到B的物件比到A的物件更好,這是上面敘述的技術的標準依據。此外,類似的引用和指標的繫結也適用於此規則,這裡省略了這些條款。

最後一個有趣的花樣是,繼承不需要必須是public的。存取規則和過載規則是不衝突的。

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

String s1(""); // 呼叫Connect(String&) Connect(s1); // 呼叫operator TemporaryString() // 接下來呼叫Connect(TemporaryString) Conncet(String("")); const String s4(""); // 呼叫operator ConstantString() const // 接下來呼叫Connect(ConstantString) Connect(s4);

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

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

首先是減少程式碼重複:Connect(String&)和Connect(ConstantString)基本上作相同的事情。上面的程式碼透過第一個過載函式呼叫第二個過載函式解決了這個問題。

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

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 > { //...建構函式,解構函式,運算子,等等... public: String(mojo::temporary< String > tmp) { String& rhs = tmp.get(); //...執行rhs到*this的析構性複製... } };

這就是傳遞函式引數的Mojo協議。

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

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

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


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

現在可以發現一個任意的型別X是按照Mojo協議設計的,只要透過mojo::traits::enabled即可確定。這個檢測機制對泛型程式設計是很重要的,很快我們就會看到它的作用。

4.2 函式返回值最佳化

現在我們可以正確的傳遞引數,讓我們看看如何將Mojo擴充套件到函式返回值最佳化。這次的目的又是具有可移植性的效率改善,即100%的消除不需要的複製而不依賴於特定的返回值最佳化(RVO)實現。

讓我們先看看通常的建議怎麼說。出於好意,一些作者也推薦返回值的使用規則[7]:

  • [規則4]當函式返回使用者定義的物件的值的時候,返回一個常量值。例如:

const String operator+(const String& lhs,const String& rhs);


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

冷靜的看,任何返回值都是短暫的,它是剛剛被建立就要很快消失的短命鬼。那麼,為什麼要強迫運算子的使用者獲得一個常量值呢?從這個觀點看,常量的臨時物件看上去就象是自相矛盾的,既是不變的,又是臨時的。從實踐的觀點看,常量物件強迫複製。

現在假定我們同意,如果效率是重要的,最好是避免返回值是常量,那麼我們如何使編譯器確信將函式的結果轉移到目的地,而不是複製他呢?

當複製一個型別為T的物件時,複製建構函式被呼叫。按照下面的設定,我們剛好可以提供這樣一個複製建構函式實現這個目標。

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


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

因為複製建構函式和其他的函式不完全相同,特別是,對一個型別X來說,在需要X(const X&)的地方定義X(X&),下面的程式碼將無法工作:

void FunctionTakingX(const X&); FunctionTakingX(X()); // 錯誤!不能發現X(const X&)


[譯註]

Wang Tianxing在gcc3.2, bcc5.5.1, icl7.0環境下測試結果表明都不會發生錯誤,並進而查閱了標準,發現Andrei是正確的,如果一定說要有什麼錯誤的話,他沒有指出這是實現定義的。

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,所以我們被迫實現String(const String&)建構函式。現在如果你允許我引用本文的話,在前面我曾經說過:“所以第一個好主意是不要宣告一個函式接受常量引用,因為它像一個黑洞一樣吞噬所有的物件。”

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

很清楚,複製建構函式需要特別的處理。這裡的想法是建立一個新的型別fnresult,那就是為String物件提供一個“轉移器(mover)”。下面是需要執行的步驟:

  1. 前面返回型別為T的值的函式現在將返回fnresult。為了使這個變化對對呼叫者透明,fnresult必須可以被隱式的轉型為T。
  2. 然後為fnresult建立轉移語義:無論何時一個fnresult物件被複制,裡面包含的T被轉移。
  3. 類似運算子的常量性和臨時性,在mojo::enabled類中為fnresult提供一個轉型運算子。
  4. 一個mojo化的類(如前例中的String)定義了一個建構函式String( mojo :: fnresult < String > )完成轉移。

這個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繼承而來,第一步值得注意,即fnresult轉型為T,然後第二個值得注意的就是複製fnresult物件的時候,隱含著它的T子物件(subobject)強制轉型為temporary

正如前面提到的,我們增加一個轉型允許返回一個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 ); };


現在考慮下面的函式:

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


在MakeString的return語句和dest的定義之間的路徑是:

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

使用RVO的編譯器可以消除呼叫鏈中fnresult(const fnresult&)的呼叫。然而,更重要的是沒有函式執行真正的複製,它們都被定義為結果的實際內容平滑的轉移到dest。也就是說沒有涉及記憶體分配和複製。

現在,正如所見,有兩個,最多三個轉移操作。當然,在一定條件和一定型別的情況下,一次複製比三次轉移可能更好。還有一個重要的區別,複製也許會失敗(丟擲異常),而轉移永遠不會失敗。

5 擴充套件

好的,我們使Mojo工作了,而且對於單獨的類相當好。現在怎樣將Mojo擴充套件到組合物件,它們也許包含大量其他的物件,而且他們中的一些已經是mojo化的。

這個任務就是將轉移建構函式從類傳遞到成員。考慮下面的例子,內嵌類String在類Widget中:

class Widget : public mojo::enabled < Widget > { String name_; public: Widget(mojo::temporary< Widget > src) // is a temporary : name_(mojo::as_temporary(src.get().name_)) { Widget& rhs = src.get(); //... use rhs to perfoa 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 ... } };


在轉移建構函式中的name_的初始化使用了一個重要的Mojo輔助函式:

namespace mojo { template < class T > struct traits { enum { enabled = Loki::SuperSubclassStrict< enabled< T >, T >::value }; typedef typename Loki::< 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做的所有事情就是根據一個左值建立一個臨時物件。使用這個方法,類成員的轉移建構函式被目標物件所呼叫。

如果String是mojo化的,Widget得到他的優點;如果不是,一個直接的複製被執行。換句話說,如果String是mojo::enabled的一個派生類,那麼as_temporary返回一個mojo::temporary。否則,as_temproary(String& src)是一個簡單的函式,帶一個String&的引數並返回同樣的String&。

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

考慮一個mojo_ptr類,它透過使複製建構函式私有而禁止它們:

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 ... } //.. };


這個類有一個有趣的行為。你不能複製這個類的常量物件。你也不能複製這個類的左值。但是你可以複製這個類的臨時物件(使用轉移語義),而且你可以顯式的移動一個物件到另外的物件:

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


這本身並沒有什麼大不了的,如果 auto_ptr 裡讓 auto_ptr(auto_ptr&)私有,也可以做到這一點。有趣的地方不是mojo_ptr本身,而是如何使用as_temporary。你可以建立高效的容器,儲存“經典”的型別、一般的mojo化的型別以及和mojo_ptr類似的型別。所有這樣的一個容器當他需要轉移元素時,必須使用as_temporary。對於“經典”型別,as_temporary是一個什麼都不做的等效函式,對於mojo_ptr,as_temporary是一個提供平滑轉移機制的函式書。move()以及uninitialized_move()的函式模板(參見所附程式碼,譯註:程式碼請到原版連結處尋找)也唾手可得。

使用標準術語,mojo_ptr既不是可以複製的,也不是可以賦值的。然而,mojo_ptr可以看作是一種新型別,叫做“可轉移的”。這是一個重要的新的分類,也許可以用於鎖(lock)、檔案(file)和其他的不可複製的控制程式碼(handle)。

如果你曾經希望一個擁有元素的類似於 vector< auto_ptr > 的容器,而且有安全、清楚的語義,現在你得到了,而且還有其他功能。另外,當包含一個複製昂貴的型別時,如vector< vector >,mojo化的vector“更能適應元素個數增減的需要”。

7 結論

mojo是一種技術,也是一個緊湊的小框架,用於消除不必要的臨時物件的複製。mojo的工作方式是檢測臨時物件並且透過函式過載操縱他們而不是簡單的作為左值。這樣做的結果是,獲得臨時物件的函式執行一個破壞性的複製,只要確信其他程式碼不再使用這個臨時物件即可。

如果客戶程式碼按照一套簡單的規則傳遞函式引數和返回值,可以應用mojo。

mojo定義了一個單獨的機制來消除函式返回時的複製。

額外的機制和型別轉換使mojo對於客戶程式碼不是100%的透明,然而對於基於庫的解決方案來說整合度是相當好的。說得好聽一點,mojo將作為一個健壯的替代品,直到一個更健壯的、基於語言特性的被標準化並實現。

8 致謝

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

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, <>.

[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》(中譯本現代C++設計正在譯製中)一書的作者。可以透過電子andrei@metalanguage.com聯絡。Andrei還是一個C++課程的有號召力的講師。

譯者的話

作為第一次編譯技術文章,而且選擇的是C++中自己相對比較陌生的主題,並且本文講述的內容是具有前瞻性的,而不是見諸於現有資料和文獻的重新整理。因此在翻譯過程中,有些細節譯者本人也沒有完全理解,因此難免出現不少差錯,歡迎大家來到newan的C++新聞組..程式設計.C語言.C++">討論。



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-993510/,如需轉載,請註明出處,否則將追究法律責任。

相關文章