发布
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();
}