CUJ:普及知識:typeint (轉)

gugu99發表於2008-05-26
CUJ:普及知識:typeint (轉)[@more@]

普及知識: typeint:namespace prefix = o ns = "urn:schemas--com::office" />

Stephen C. Dewhurst

 (WQ注:這是比Loki還令我震驚的東西,實在難以譯好。先放出來讓大家都震驚一下,以後我會修訂的。)

--------------------------------------------------------------------------------

  在最近的系列文章中,我們為 C++語言設計和實現了一個typeof功能[注1]。 實現中要求在編譯期將的型別轉換為一定結構的整型值,而這個整型值在以後還可以解碼回原始的型別。我們遇到的問題是編碼受到了整型值的長度限制,因為為了達到必須的編譯期,演算法將限定在整型的精度範圍(32或64bits)內。而許多“合理的” 型別,編碼需要數百bits長度的整型值。 (“不合理的” 型別可能需要極大的整型值)。

因為通常使用的資料抽象技術在編譯期不是可用的,我們無法實現一個擴充套件精度的整型型別;我們被限定於可以使用在整型常量表示式上的那些操作上。我們的方法是實現一個編譯期算術運算,操縱預定義的整型數集合。比如,左移一個4元組整型值(由4個常量表示式c1-c4組合而成),我們可以使用類似於下面的程式碼:

typedef ShiftLeft SL;

enum {

  code1 = SL::code1,

  code2 = SL::code2,

  code3 = SL::code3,

  code4 = SL::code4

};

這是可行的,但很難看。 我們也不得不選擇預定義最大精度(4,對於本例), 而不讓編碼的複雜度在暗中受精度的影響。

  如我們上面所提到的,的強項和弱處都是受被它們編譯的語言所提的需求決定的。一個C++語言編譯器只需要最基本的編譯期算術的能力,但是需要廣泛的操縱繁雜的型別的能力。 事實上,多數設計良好的編譯器都一個“型別的數學”的概念,於是,型別在編譯期被用各種不同的“型別的數學”的運算來操縱。舉例來說,當解析一個名稱的宣告時,編譯器從宣告的限定符和宣告符中組合出名稱的型別。當名稱在表示式中被操縱時,它的型別將會同時被其它的“型別的數學”的運算所被操縱。

  這暗示了可能可以這樣做:用整型值表示型別,將型別的操縱運算對映為編譯期數學運算,以擴大編譯器在這個領域的廣泛能力。我們然後應該能夠將擴充套件精度的編譯期演算法施加在這個“整型/型別”的抽象上。 如果需要,擴充套件精度的結果的一部分可以被對映回相應的完整型別上。

 

對映整型值到型別

定義一個從C++的型別到無符號二進位制整型的簡單對映:為0的位表示一個指標,為1的位表示const的指標。 指標的基礎型別無關緊要的,因此,我們選擇char來示意。沒有任何限定/修飾的基礎型別將會表現為數值零點。 為了支援變長的typeint,我們不允許在這個型別表示法中出現前導的0位(即沒有const限定的普通指標符) (WQ注:也就是說,01011100會自動濾除前導0而成1011100)。

舉例來說,藉由這個簡單的編碼法則,我們能用無符號二進位制整數1011100(十進位制是92) 表示型別char *const * *const *const *const * *。讓我們把這個類似於整型的型別叫做typeint。

很容易將一個小的整型值轉換為一個typeint[注2]:

template

struct TypeInt {

  typedef typename <

  n&1,

  typename TypeInt>1)>::R *const,

  typename TypeInt>1)>::R *

  >::R R;

};

 

template <>

struct TypeInt<0>

  { typedef char R; };

TypeInt模板主體透過遞迴決定整型值n的高位的編碼結果,然後追加上一個* const(如果低位為1)或*(如果低位為0)。 遞迴結束於TypeInt對0的特化。

將typeint轉換成一個整數也很直觀,如果它對應的整型值能夠對應到一個預定義的整數上的話:

template

struct Value

  { enum { r = 0 }; };

 

template

struct Value

  { enum { r = Value::r*2 }; };

 

template

struct Value

  { enum { r = Value::r*2 + 1 }; };

在這裡,模板的主體用於結束遞迴,此時所有的“位”(指標指示符) 都已從 typeint上分離。 偏特化將遞迴計算從typeint上反引用得到的整數的等價物,然後根據最遠的指標指示符追加一個0或1的位。

cout << Value< TypeInt<92>::R >::r << endl; // 92

 

對typeint進行移位

左移應該補零,因此我們只須附加正確數目的0:

template

struct LShift

  { typedef typename LShift::R *R; };

 

template

struct LShift

  { typedef T R; };

然而,我們的typeint 表示法不允許前導的0, 因此我們必須特別處理可能發生的為0(char)的typeint的移位:

template

struct LShift

  { typedef char R; };

最後,我們必須提供一個特化以消除為0的typeint左移0位時的歧義。否則的話,將會在偏特化間發生二義性。

template <>

struct LShift

  { typedef char R; };

在這個實現上,右移總是補0。因為基於這個實現,傳入的引數上是沒有前導的0的,這意謂著只需簡單地反引用typeint適當的次數[注3]:

template

struct RShift {

  typedef typename Deref::R>::R R;

};

 

template

struct RShift {

  typedef T R;

};

 

位運算

讓我們看一下“位或”運算的實現。“位加”和其它位運算是類似的。 在這個實現中,我們使用一個編譯期的switch...case來區分一個或兩個引數都是0的情況:

template

struct Or {

  enum { switchval = IsPtr::r*2 + IsPtr::r };

  typedef typename OrImpl::R R;

};

既然一個為零的typeint表示無修飾的char基礎型別,我們能構造一個switch...case表示式,以判斷引數的位值是否都為0(都是指標時,值為3),或者只第一個引數非0(2),或者只第二個引數是非0(1)(WQ注,原文為only the second argument is zero ,應筆誤),或者兩個引數都是0(0)。 你可能會料到,一個或兩個引數為0的情況很簡單:

template

struct OrImpl<0,A,B> {

  typedef char R; // 0 | 0 == 0

};

template

struct OrImpl<2,A,B> {

  typedef A R; // A | 0 == A

};

template

struct OrImpl<1,A,B> {

  typedef B R; // 0 | B == B

};

當兩個引數都非0時,就比較有趣了:

template

struct OrImpl<3,A,B> {

  typedef typename Deref::R DA;

  typedef typename Deref::R ;

  typedef typename Or::R Temp;

  typedef

  typename Select<

  // either A or B has a 1 bit

  Inst::r || IsConst::r,

  Temp * const,

  Temp *

  >::R R;

};

首先, 我們反引用兩個引數,以移除它們的低位,並遞迴計算已被縮短的typeint的“位或”結果。 然後追加上被較早移除的低位的“位或”結果。

編譯期的switch...case看起來是不必要的複雜。另一個解決方法是使用徹底的特化。有九種情況需要考慮,列於下表。

First Argument

Second Argument

Char

char

Char

B *

Char

B * const

A *

char

A *

B *

A *

B * const

A * const

char

A * const

B *

A * const

B * const

 

根據上表,模板主體和8個偏特化如下:

template

struct Or { typedef char R; };

 

template

struct Or { typedef typename Or::R *R; };

 

template

struct Or {

  typedef typename Or::R *const R;

};

// 5 more cases...

template

struct Or { typedef A *const R; };

在“位或”的實現中,兩個實現半斤八兩。在“位與”的實現中,可以合併一些情況,所以實現徹底的特化將更容易些。

template

struct And // at least one of A or B is 0

  { typedef char R; };

 

template

struct And

  { typedef typename And::R *R; };

 

template

struct And

  { typedef typename And::R *R; };

 

template

struct And

  { typedef typename And::R *R; };

 

template

struct And

  { typedef typename And::R *const R; };

 

 

 

使用和限制

用typeint擴充套件編譯期演算法比我們的較早的機制佔優勢。 最初的機制是羅嗦的,並且固定在一個給定的精度上:

 

typedef ShiftLeft SL;

enum {

  code1 = SL::code1,

  code2 = SL::code2,

  code3 = SL::code3,

  code4 = SL::code4

};

typeint用起來比較簡潔,而且不需要事先指定結果的精度。typeint 會在需要時擴充套件自己的精度,直到編譯器的能力極限:

typedef LShift::R Code;

不幸地是,存在一個問題。typeint的實現是基於深度遞迴的,而很多編譯器不能對遞迴的模板例項化到足夠的深度。這嚴重限制了typeint的最大位長。雖然 C++語言標準建議的最小例項化深度只有17層 (在標準的 Annex B),但大多數編譯器看起來都能夠處理至少60層,而有些還有編譯選項以允許高達600-700層,更有一些則可一直例項化到資源耗盡(通常結果是達到幾千層)。 我們將在以後的專欄文章中尋找方法來處理遞迴模板例項化深度的限制。

 

複雜的乘法

在結束前,讓我們瞄一下typeint的乘法的實現。我們用傳統的移位相加的方法來實現它:

 

  1 1 0 1 // A

  x  1 0 0 1 // B

  ------------

  1 1 0 1

  0 0 0 0

  0 0 0 0

 1 1 0 1

---------------

 1 1 1 0 1 0 1 // A * B

typeints 的計算的本質上是上面是相同的,但是可讀性比較差:

   char *const *const * *const


  x  char *const * * *const


  ---------------------------------------


  char *const *const * *const


  char


  char


  char *const *const * *const * * *


-------------------------------------------


char *const *const *const * *const * *const


移位相加乘法的直接實現卻還簡潔得令人有些心慰:

template

struct Mul;  // no definition of primary

 

template

struct Mul  // A * 0 == 0

  { typedef char R; };

 

template

struct Mul  // 0 digit, just shift

牋牋牋牋牋牋牋牋  // A*B0 == (A*B)<<1

  { typedef typename Mul::R *R; };

 

template

struct Mul {  // 1 digit, shift and add

牋牋牋牋牋牋牋牋牋?  // A*B1 == ((A*B)<<1)+A

  typedef typename Mul::R *Tmp;

  typedef typename Add::R R;

};

 

template

struct Mul  // to avoid a char * typeint

  { typedef A R; };  // no leading zeros!

不幸地是,這個演算法有二次方的(編譯期!) 複雜度,而不是其它運算的線性複雜度。結果,當對35位的typeint進行乘法時,在典型的工作站上,有一個引人注目的編譯延遲,而且對200位的typeint進行乘法時至少需要一個午休時間。

 

預告

下次,我們將會詳細檢視乘法的實現,並考察許多template metaprogramming 技術,它們能戲劇性地提高效能。再下次,將探索解決模板例項化深度限制的方法。

 

[1] S.C. Dewhurst, "Common Knowledge: A Bitwise typeof Operator, Parts 1-3," C/C++ Users Journal, August, October, and December 2002.

 

[2] The full implementation of this preliminary typeint facility is available on the author's site: < The reader may find in the same location a reimplementation of the typeof facility that employs typeints for extended-precision compile-time arithmetic. The Select template is borrowed (in modified form) from Andrei Alexandrescu's Loki library. It performs a compile-time if statement, selecting its second argument if the first argument is true, and its third argument if the first argument is false.

 

[3] Description of utilities like Select, IsPtr, and Deref are omitted for reasons of space. Their implementations are available as described in the previous note.


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

相關文章