釋出
2010 年 1 月 27 日(最後更新:2010 年 1 月 28 日)

C++ 型別擦除

評分:4.3/5(263 票)
*****
模板相對於多型性的一個顯著優勢
是它們能夠保留型別。當你編寫一個 frobnicate() 函式
它接受一個基類例項(透過引用或指標),該函式
“丟失”了其引數的真實型別:從
編譯器的角度來看,在執行 frobnicate() 時,它有一個基類例項。

示例

1
2
3
4
5
6
7
8
9
10
11
struct Base {};
struct Derived : Base {};

void frobnicate( Base& b ) {
   std::cout << "The compiler says this function was passed a base class object";
}

int main() {
   Derived d;
   frobnicate( d );
}


儘管實際上我們向 frobnicate() 傳遞了一個 Derived 例項,但從
編譯器的角度來看,frobnicate() 收到的是一個 Base 例項,而不是一個
Derived 例項。

這有時可能會成為一個問題。也許最常見的麻煩點是當
frobnicate() 需要對傳遞給它的物件進行複製。它不能。
首先,要複製一個物件,你必須在編譯時知道它
的真實型別,因為真實型別名稱用於呼叫複製建構函式

1
2
// Note how we must say "new type-name".
Derived* d_copy = new Derived( original_d );


(事實上,克隆模式就是為了解決這個問題而發明的。但我們不會
在這裡討論它。)

模板解決了這個問題,因為模板允許你在編譯時保留 b 的
“真實型別”。

1
2
3
4
5
template< typename T >
void frobnicate( const T& b ) {
   T b_copy( b );
   std::cout << "I just made a copy of b" << std::endl;
}



現在我已經稍微宣揚了一下模板,應該明白有時,
模板保留型別的能力是一種阻礙。這怎麼可能呢?考慮
以下宣告

1
2
3
template< typename T >
class MyVector {
};


此宣告的問題在於它將包含的型別作為其型別的一部分暴露出來:
MyVector<int> 與 MyVector<unsigned> 不是同一種類型
也與 MyVector<char> 不是同一種類型。例如,如果我們要將
MyVector 例項儲存在 STL 容器中,我們不能直接這樣做,因為
除非你建立一個基類並存儲指向基類例項的指標,否則容器不支援多型性。
但是,這樣做可能會導致
上述丟失型別資訊的問題,並且還會增加程式碼的緊密耦合,
因為現在兩個可能不相關的型別必須符合由通用基類定義的某些虛擬介面。
由公共基類定義的某些虛介面。







引入型別擦除。以上述 MyVector 為例(它在這裡不是一個很好的例子,
但它說明了這一點),如果 MyVector 不需要將 T 作為其型別的一部分暴露出來怎麼辦?
那麼,我就可以將一個 MyVector of ints 儲存在與一個 MyVector of std::strings
相同的容器中,而無需訴諸派生和多型性。
派生和多型性。

事實上,boost::any 是型別擦除的一個很好的例子。boost::any 允許你
在它裡面儲存任何東西,但 boost::any 本身不是一個模板
類——它不暴露其內部所包含內容的型別。

boost::function 是型別擦除的另一個例子。

但它能為你做什麼?為什麼要費心?

讓我們以一個 RPG 遊戲為例。遊戲中有各種各樣的物品:
各種型別的武器、各種型別的盔甲、各種型別的頭盔、
卷軸、魔法藥水等等。我希望能夠將所有這些物品
儲存在我的揹包裡。立刻想到一個 STL 容器——也許是一個 deque。
但這意味著我必須建立一個名為 Item 的類,它是所有不同種類物品屬性的超集,
或者我必須將 Item 設為所有這些型別的基類。但是,一旦我將 Item 儲存在揹包中,
我就會丟失它的真實型別。如果我想阻止玩家,比如說,
將卷軸作為武器揮舞,或者將手電筒作為盔甲穿戴,我必須訴諸於向下轉型
來檢查該物品是否確實是正確的型別。
來檢查該物品是否確實是正確的型別。

但還有另一種選擇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Weapon {};
class Armor {};
class Helmet {};
class Scroll {};
class Potion {};

class Object {
   struct ObjectConcept {
       virtual ~ObjectConcept() {}
   };

   template< typename T > struct ObjectModel : ObjectConcept {
       ObjectModel( const T& t ) : object( t ) {}
       virtual ~ObjectModel() {}
     private:
       T object;
   };

   boost::shared_ptr<ObjectConcept> object;

  public:
   template< typename T > Object( const T& obj ) :
      object( new ObjectModel<T>( obj ) ) {}
};

int main() {
   std::vector< Object > backpack;

   backpack.push_back( Object( Weapon( SWORD ) ) );
   backpack.push_back( Object( Armor( CHAIN_MAIL ) ) );
   backpack.push_back( Object( Potion( HEALING ) ) );
   backpack.push_back( Object( Scroll( SLEEP ) ) );
}


現在我能夠將不同型別的物件儲存在我的揹包中。
憤世嫉俗者會爭辯說我沒有儲存多型型別;我正在儲存
物件。是的……也不全是。正如我們將看到的,Object 是一個簡單的“直通”
物件,它稍後會對程式設計師透明。

但是,你說,你只是做了繼承的事情。這有什麼更好的呢?
它更好並不是因為它比繼承方法提供更多的功能,
而是因為它沒有透過共同的基類緊密耦合武器和盔甲等。它賦予我
保留型別的能力,就像模板一樣。

假設我現在想檢視所有在戰鬥中能夠對敵人造成傷害的物品。
嗯,所有武器都可以,也許還有一些,但不是所有卷軸和藥水都可以。
火之卷軸會傷害敵人,而附魔盔甲卷軸則不會。
附魔盔甲卷軸則不然。

這是一種方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
   
struct Weapon {
   bool can_attack() const { return true; } // All weapons can do damage
};

struct Armor {
   bool can_attack() const { return false; } // Cannot attack with armor...
};

struct Helmet {
   bool can_attack() const { return false; } // Cannot attack with helmet...
};

struct Scroll {
   bool can_attack() const { return false; }
};

struct FireScroll {
   bool can_attack() const { return true; }
}

struct Potion {
   bool can_attack() const { return false; }  
};


struct PoisonPotion {
   bool can_attack() const { return true; }
};


class Object {
   struct ObjectConcept {   
       virtual ~ObjectConcept() {}
       virtual bool has_attack_concept() const = 0;
       virtual std::string name() const = 0;
   };

   template< typename T > struct ObjectModel : ObjectConcept {
       ObjectModel( const T& t ) : object( t ) {}
       virtual ~ObjectModel() {}
       virtual bool has_attack_concept() const
           { return object.can_attack(); }
       virtual std::string name() const
           { return typeid( object ).name; }
     private:
       T object;
   };

   boost::shared_ptr<ObjectConcept> object;

  public:
   template< typename T > Object( const T& obj ) :
      object( new ObjectModel<T>( obj ) ) {}

   std::string name() const
      { return object->name(); }

   bool has_attack_concept() const
      { return object->has_attack_concept(); }
};

int main() {
   typedef std::vector< Object >    Backpack;
   typedef Backpack::const_iterator BackpackIter;

   Backpack backpack;

   backpack.push_back( Object( Weapon( SWORD ) ) );
   backpack.push_back( Object( Armor( CHAIN_MAIL ) ) );
   backpack.push_back( Object( Potion( HEALING ) ) );
   backpack.push_back( Object( Scroll( SLEEP ) ) );
   backpack.push_back( Object( FireScroll() ) );
   backpack.push_back( Object( PoisonPotion() ) );

   std::cout << "Items I can attack with:" << std::endl;
   for( BackpackIter item = backpack.begin(); item != backpack.end(); ++item )
       if( item->has_attack_concept() )
           std::cout << item->name();
}