Generic:型別和值之間的對映 (轉)

worldblog發表於2007-12-13
Generic:型別和值之間的對映 (轉)[@more@]

Generic:型別和值之間的對映
Andrei Alexandrescu

在C++中,術語“轉化”(conversion)描述的是從另外一個型別的值(value)獲取一個型別(type)的值的過程。可是有時候你會需要一種不同型別的轉化:可能是在你有一個型別時需要獲取一個值,或是其它的類似情形。在C++中做這樣的轉化是不尋常的,因為型別域和值域之間隔有有一堵很嚴格的界線。可是,在一些特定的場合,你需要跨越這兩個邊界,本欄就是要討論該怎麼做到這個跨越。

對映整數為型別

一個對許多的generic programming風格非常有幫助的暴簡單的模板:

template
struct Int2Type
{
  enum { value = v };
};

對傳遞的每一個不同的常整型值,Int2Type“產生”一個不同的型別。這是因為不同的模板的實體(instantiation)是不同的型別,所以Int2Type<0>不同於Int2Type<1>等其它的型別的。此外,產生型別的值被“存放”在列舉(enum)的成員值裡面。

不管在任何時候,只要你需要“型別化”(typify)一個整型常數時,你都可以使用Int2Type。比如這個例子,你要設計一個NiftyContainer類别範本。

template
class NiftyContainer
{
  ...
};

NiftyContainer了指向T的指標。在NiftyContainer的一些成員(member functions)中,你需要克隆型別 T的,如果T是一個非多型的型別,你可能會這樣說:

T* pSomeObj = ...;
T* pNewObj = new T(*pSomeObj);

對於T是多型型別的情形,情況要更為複雜一些,那麼我們假定你建立了這樣的規則,所有的使用於NiftyContainer 的多型型別必須定義一個Clone虛擬函式(virtual function)。那麼你就可以像這樣來克隆物件:

T* pNewObj = pSomeObj->Clone();

因為你的容器(container)必須能夠接受這兩種型別,所以你必須實現兩種克隆演算法並在編譯時刻選擇適當的一個。那麼不管透過NiftyContainer的布林(非型別,non-type)模板引數傳遞的型別是不是多型的,你都要和它互動,而且還要依賴員給它傳遞的是正確的標識。

template
class NiftyContainer
{
  ...
};

NiftyContainer widgetBag;
NiftyContainer numberBag;

如果你儲存在NiftyContainer裡的型別不是多型的,那麼你就可以對NiftyContainer的許多成員函式進行處理,因為可以藉助於常量的物件大小(constant size)和值語義(value semantics)。在所有的這些成員函式中,你需要選擇一個演算法,或是另外一個依賴於模板引數isPolymorphic的演算法。

乍一看,似乎只用一個if語句就可以了。

template
class NiftyContainer
{
  ...
  void DoSomething(T* pObj)
  {
  if (isPolymorphic)
  {
  ... polymorphic algorithm ...
  }
  else
  {
  ... non-polymorphic algorithm ...
  }
  }
};

問題是是不會讓你擺脫這些程式碼的。例如,如果多型演算法使用了pObj->Clone,那麼NiftyContainer::DoSomething就不會那些任何一個沒有定義Clone成員函式的型別而編譯。的確,看起來在編譯時刻要哪一個if語句分支是很明顯的,但是這不關編譯器的事,編譯器仍然堅持不懈地盡心盡職地編譯這兩個分支,即使最佳化器最終會消除這些廢棄程式碼(dead code)。如果你試圖NiftyContainer的DoSomething函式的話,編譯器就會停留在pObj->Clone的呼叫之處,這是怎麼回事?

等等,問題還多著呢。如果T是一個多型型別,那麼程式碼將又一次不能透過編譯了。如果T將它的copy constructor設為private和protected,禁止外部對其訪問——作為一個行為良好的多型類,應該如此。那麼,如果非多型的程式碼分支要做new T(*pObj),則程式碼不能編譯透過。

如果編譯器不為編譯廢棄程式碼費神那多好啊,但無望的期望不是解決之道,那麼怎樣才是一個滿意的解決方案呢?

已經證實,有許多的解決辦法。Int2Type就提供了一個非常精巧的解決方案。對應於isPolymorphic的值為true和false,Int2Type可以將特定的布林值isPolymorphic轉化為兩個不同的型別。那麼你就可以透過簡單的過載(overloading)來使用Int2Type了,搞定!

“整型型別化”(integral typifying)風格的原型(incarnation)如下所示:

template
class NiftyContainer
{
private:
  void DoSomething(T* pObj, Int2Type)
  {
  ... polymorphic algorithm ...
  }
  void DoSomething(T* pObj, Int2Type)
  {
  ... non-polymorphic algorithm ...
  }
public:
  void DoSomething(T* pObj)
  {
  DoSomething(pObj, Int2Type());
  }
};

這個程式碼簡單扼要,DoSomething呼叫過載了的私有成員函式,根據isPolymorphic的值,兩個私有過載函式之一被呼叫,從而完成了分支。這裡,型別Int2Type的虛擬臨時變數沒有被用到,它只是為傳遞型別資訊之用。

不要太快了,天行者!

看到上面的方法,你可能認為還有更為巧妙的解決之道,可以使用比如template specialization這樣的技巧。為什麼必須用虛擬的臨時變數,一定還有更好的方式。但是,令人驚奇的是,在簡單性、通用性和上,Int2Type是很難打敗的。

一個可能的嘗試是,根據任意的T及isPolymorphic的兩個可能的值,對NiftyContainer::DoSomething作特殊處理。這不就是partial template specialization的拿手戲嗎?

template
void NiftyContainer::DoSomething(T* pObj)
{
  ... polymorphic algorithm ...
}

template
void NiftyContainer::DoSomething(T* pObj)
{
  ... non-polymorphic algorithm ...
}

看上去很美,可是啊呀,不好,它是不合法的。沒有這樣的對一個類别範本的成員函式進行partial specialization的方式,你可以對整個NiftyContainer作partial specialization:

template
class NiftyContainer
{
  ... non-polymorphic NiftyContainer ...
};

你也可以對整個DoSomething作specialization:

template <>
void NiftyContainer::DoSomething(int* pObj)
{
  ... non-polymorphic algorithm ...
}

但奇怪的是,在[1]之間,你不能做任何事。

另一個辦法可能是引入traits技術[2],並在NiftyContainer的外部來實現DoSomething(在traits類中),但把DoSomething分開來實現顯得有些笨拙了。

第三個辦法仍然試圖用traits技術,但把實現都放在一起,這就要在NiftyContainer裡面把traits定義為私有的內部類。總之,這是可以的,但在你設法實現的時候,你就會認識到基於Int2Type的風格有多好。而且這種風格最好的地方可能就在於:在實際應用中,你可以把這個小小的Int2Type模板放在庫中,並把它的預期使用記錄在案。

型別到型別的對映

考慮下面這個函式:

template
T* Create(const U& arg)
{
  return new T(arg);
}

Create透過傳遞一個引數給T的建構函式(constructor)而產生了一個新的物件。

現在假設在你的應用中用這麼一個規則:型別Widget的物件是遺留下來的程式碼,在構造時必須要帶兩個引數,第二個引數是一個像-1這樣的固定值。在所有派生自Widget的類中你不會碰到什麼問題。

你要怎麼對Create作特殊化處理,才能讓它在處理Widget時,不同於所有的其它型別呢?你是不可以對函式作partial specialization的,也就是說,你不能像這樣做:

// Illegal code - don't try this at home
template
Widget* Create(const U& arg)
{
  return new Widget(arg, -1);
}

由於缺乏函式的partial specialization,我們所擁有的唯一工具,還是過載。可以傳遞一個型別T的虛擬物件,並過載。

// An implementation of Create relying on overloading
template
T* Create(const U& arg, T)
{
  return new T(arg);
}
template
Widget* Create(const U& arg, Widget)
{
  return new Widget(arg, -1);
}
// Use Create()
String* pStr = Create("Hello", String());
Widget* pW = Create(100, Widget());

Create的第二個引數只是為作選擇適當的過載函式之用,這也是這種自創風格的一個問題所在:你把時間浪費在建構一個你不使用的強型別的複雜物件上,即使最佳化器可以幫助你,但如果Widget遮蔽掉default construtor的話,那最佳化器也愛莫能助了。

The proverbial extra level of indirection can help here, too. (不好意思,這句不知道怎麼翻譯)一個想法是:傳遞T*而不是T來作為虛擬的模板引數。在執行時刻,總是可以傳遞空指標的,這在構造時的代價是相當低廉的。

template
T* Create(const U& arg, T*)
{
  return new T(arg);
}
template
Widget* Create(const U& arg, Widget*)
{
  return new Widget(arg, -1);
}
// Use Create()
String* pStr = Create("Hello", (String*)0);
Widget* pW = Create(100, (Widget*)0);

這種方式對於Create的使用者來說,是最具迷惑性的。為了保持這種解決風格,我們可以使用同Int2Type有一些類似的模板。

template
struct Type2Type
{
  typedef T OriginalType;
};

現在你可以這樣寫了:

template
T* Create(const U& arg, Type2Type)
{
  return new T(arg);
}
template
Widget* Create(const U& arg, Type2Type)
{
  return new Widget(arg, -1);
}
// Use Create()
String* pStr = Create("Hello", Type2Type());
Widget* pW = Create(100, Type2Type());

比起其它的解決方案來說,這當然更加說明問題。當然,你又得在庫裡面包含Type2Type並將在它預期使用的地方記錄在案。

檢查可轉化性和繼承

在實現模板函式和模板類時,經常會碰到這樣的問題:給出兩個強型別B和D,你怎麼檢查D是不是派生自B的呢?

在對通用庫作進一步的最佳化時,在編譯時刻發現這樣的關係是一個關鍵。在一個通用函式里,如果一個類實現了一個特定的介面,你就可以藉助於一個最佳化的演算法,而不必對其作一個dynamic_cast。

檢查派生關係藉助於一個更為通用的機制,那就是檢查可轉化性。同時,我們還要解決這樣一個更為普遍的問題:要怎樣才能檢查一個強型別T是否支援自動轉化為一個強型別U?

這個問題有一個解決辦法,它可藉助於sizeof。(你可能會想,sizeof不就是用作memset的引數嗎,是吧?)sizeof有相當多的用途,因為你可以將sizeof使用到任何一個(expression)中,在執行時刻,不管有多複雜,不用計較表示式的值是多少,sizeof總會返回表示式的大小(size)。這就意味著sizeof知道過載、模板實體化和轉化的規則——每一個參與C++表示式的東東。實際上,sizeof是一個推導表示式型別的功能齊備的工具。最終,sizeof撇開表示式並返回表示式結果的大小[3]。

可轉化性檢查的想法藉助於對過載函式使用sizeof。你可以巧妙地對一個函式提供兩個過載:一個接受可以轉化為U的型別,另一個則可以接受任何型別。這樣你就可以呼叫以T為引數的過載函式了,這裡T是你想要的可以轉化為U 的型別。如果傳入的引數為U的函式被呼叫,則T就轉化為U;如果“退化”(fallback)函式被呼叫,則T不轉化為U。

為了檢查是哪一個函式被呼叫了,你可以讓兩個過載函式返回不同大小的型別,並用sizeof對其進行鑑別。只要是不同大小的型別,它們就難逃法眼。

首先建立兩個不同大小的型別(顯然,char和long double有不同的大小,但標準沒有作此擔保),一個簡單的程式碼摘要如下所示:

typedef char Small;
struct Big { char dummy[2]; };

由定義可知,sizeof(Small)大小為1,Big的大小是未知的,但肯定是大於1的,對於我們來說,能保證這個已經足夠了。

下一步,需要做兩個過載,一個接受U並返回一個Small。

Small Test(U);

那你要怎樣來寫一個可以接受任何“東東”的函式呢?模板解決不了這個問題,因為模板會尋找最匹配的一個,由此而隱藏了轉化。我們需要的是一個比自動轉化要更差一些的匹配。快速瀏覽一下應用於給定了省略號的函式呼叫的轉化規則。它就在列表的最後面,就是最差的那個,這恰恰就是我們所需要的。

Big Test(...);

(以一個C++物件來呼叫一個帶省略符的函式會產生未知的結果,可誰在乎呢?又不會有人真的呼叫這樣的函式,這種函式甚至都不會被實現。)

現在我要對Test的呼叫使用sizeof,給它傳遞一個T:

const bool convExists =
  sizeof(Test(T())) == sizeof(Small);

就是它!Test呼叫產生了一個預設構造物件T,然後sizeof提取了表示式的結果的大小。它可能是sizeof(Small),也可能是sizeof(Big),這要看編譯器是否找到了轉化的可能。

還有一個小問題,如果T把它的預設建構函式作為私有成員會怎麼樣?在這種情況下,T的表示式將不能透過編譯,我們所構建的所有的這一切都是白費。幸好,這又一個簡單的解決方案——只要使用一個能返回T的像稻草人一樣沒用的函式就好了。這樣,一切問題統統解決!

T MakeT();
const bool convExists =
  sizeof(Test(MakeT())) == sizeof(Small);

(By the way, isn't it nifty just how much you can do with functions, like MakeT and Test, which not only don't do anything, but which don't even really exist at all?)
(順便說一下,就像MakeT和Test一樣,這類函式是好是壞取決於你用它來做什麼,它不但什麼都不做,而且甚至根本不存在的?)

現在我們可以讓它工作了,把一切都封裝到一個類别範本中,隱藏所有關於型別推導的細節,只把結果暴露出來。

template
class Conversion
{
  typedef char Small;
  struct Big { char dummy[2]; };
  static Small Test(U);
  static Big Test(...);
  T MakeT();
public:
  enum { exists =
  sizeof(Test(MakeT())) == sizeof(Small) };
};

現在你可以這樣來測試Conversion模板類了。

int main()
{
  using namespace std;
  cout
  << Conversion::exists << ' '
  << Conversion::exists << ' '
  << Conversion >::exists << ' ';
}

這個小程式的列印結果為“1 0 0”。我們注意到,儘管std::vector實現了一個帶引數為size_t的建構函式,轉化測試返回的結果還是0,因為建構函式是顯式的(explicit)。

我們可以在Conversion中實現這樣的兩個或是更多的常量(constants)。

exists2Way表示在T和U之間是否可以相互轉化。例如,int和double就是可以相互轉化的情況,但是各種自定義型別也可以實現這樣的相互轉化。
sameType表示T和U是否是同種型別。

template
class Conversion
{
  ... as above ...
  enum { exists2Way = exists &&
  Conversion::exists };
  enum { sameType = false };
};

我們透過對Conversion作partial specialization來實現sameType。

template
class Conversion
{
public:
  enum { exists = 1, exists2Way = 1, sameType = 1 };
};

那麼,怎麼來做派生關係的檢查呢?最漂亮的地方就在於此,只要你把轉化處理好了,派生關係的檢查就簡單了。

#define SUPERSUBCLASS(B, D)
  (Conversion::exists &&
  !Conversion::sameType)

是不是一目瞭然了?可能還有一點點的迷糊。SUPERSUBCLASS(B, D)判斷D公有派生自B是否為真,或者B和D代表的是同種型別。透過判別一個const D*到const B*的可轉化性,SUPERSUBCLASS就可以作出這樣的判斷。const D*隱式轉化為const B*只有三種情況:

B和D是同種型別;
B是D的明確的公有基類;
B是空型別。
透過第二個測試可以排除最後一種情形。在實際應用中,第一種情形(B和D為同種型別)的測試是很有用的。因為出於實際考慮,你通常都會考慮一個類是它自己的超類。如果你需要一個更嚴格的測試,可以這樣寫:

#define SUPERSUBCLASS_STRICT(B, D)
  (SUPERSUBCLASS(B, D) &&
  !Conversion::sameType)

為什麼這些程式碼中都加上了const修飾呢?原因是你總不想因為const的問題而讓轉化測試失敗吧。所以,在每個地方都使用了const,如果模板程式碼使用了兩次const(對一個已經是const的型別使用const),則第二個const將被忽略掉。簡而言之,在SUPERSUBCLASS中使用const是基於考慮的。

Why SUPERSUBCLASS and not the cuter BASE_OF or INHERITS? For a very practical reason: with INHERITS(B, D), I kept forgetting which way the test works — does it test whether B inherits D or vice versa? SUPERSUBCLASS(B, D) makes it clearer (at least for me) which one is first and which one is second.
為什麼用SUPERSUBCLASS而不是更為貼切的BASE_OF或是INHERITS呢?為了一個實際的原因:使用INHERITS(B, D),我會經常忘記檢測的運作方式——它測試的是B派生自D呢,還是D派生自B?而對這個問題(誰是第一個誰是第二個),SUPERSUBCLASS(B, D)說明得更為清楚一些(至少對於我來說)。

小結

在這裡介紹這三種風格,一個最重要的地方就是它們是可重用的。你可以把它們寫在一個庫裡面,並可以讓程式設計師們使用它們,而不是要他們掌握這其中複雜的內部實現工作。

nontrivial技術的可重用性是很重要的,要人們記住一個複雜的技術,即使這個技術可以用來幫助他們的實際工作更為簡化一些,但如果這個技術稍顯麻煩,他們也是不會用的。給人們一個簡單的黑盒,它可以帶來一些有用的驚奇,人們是會喜歡它並使用它的,因為這是一個“自由”的方式。

Int2Type,Type2Type,特別是Conversion都屬於一個通用的工具庫。透過使用重要的編譯時刻的型別推導,它們擴充套件了程式設計師的編譯時刻的能力。

致謝

如果Clint Eastwood問我:“你感覺幸運嗎?”,我的回答肯定為“是”。這是我這個系列的第三篇文章,這得益於 Herb Sutter的直接的關注和重要的建議。感謝日本的Tomio Hoshida發現了一個並做了一些有深刻見解的建議。

注:這篇文章引用自Andrei Alexandrescu的即將出版的一本書,書名暫定為《Modern C++ Design》(Addison-Wesley, 2001)。(譯註:這本書已經出版了,要是能早日拜讀,那有多爽啊)
註釋
[1]C++對函式的partial specialization支援是沒有概念上的障礙的,這是一個很有價值的特性。
[2] Andrei Alexandrescu. "Traits: The else-if-then of types", C++ Report (April 2000).
[3]建議在C++中加入一個型別of運算子,也就是一個返回一個表示式的型別的運算子。有了這麼一個運算子,將會使編寫模板程式碼更加易於編寫、易於理解。 C++已經把typeof作為一個擴充套件實現了。明顯地,typeof和sizeof有同樣的障礙,因為無論如何,sizeof都必須計算型別。[參閱Bill Gibbons在CUJ上的一篇文章,他介紹了一個漂亮的方法,這個方法用於在現在的Standard C++裡創造一個(幾乎是)天然的typeof運算子。]


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

相關文章