One of the significant advantages templates have over polymorphism
is their ability to retain type. When you write a function frobnicate()
that takes a base class instance (by reference or pointer), that function
has "lost" the real type of its parameter: from the perspective of
the compiler, while executing frobnicate(), it has a base class instance.
Example:
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 );
}
| |
Although in reality we passed a Derived instance to frobnicate(), from the
perspective of the compiler, frobnicate() received a Base instance, not a
Derived instance.
This can be a problem at times. Perhaps the most common trouble spot is when
frobnicate() needs to make a
copy of the object passed to it. It can't.
First of all, to copy an object you have to know
at compile time it's
real type, because the real type name is used to invoke the copy constructor:
1 2
|
// Note how we must say "new type-name".
Derived* d_copy = new Derived( original_d );
| |
(In fact, the clone pattern was invented to solve this problem. But we won't
go into that here.)
Templates solve that problem, because templates allow you to retain the
"real type" of b at compile time.
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;
}
| |
Now that I've evangelized templates a bit, it should be understood that sometimes,
templates' ability to retain type is a hindrance. How can that be? Consider
the following declaration:
1 2 3
|
template< typename T >
class MyVector {
};
| |
The problem with this declaration is that it exposes the contained type as part
of
its type: MyVector<int> is not the same type as MyVector<unsigned>
is not the same type as MyVector<char>. If we wanted, for example, to store
MyVector instances in an STL container, we couldn't directly, because the
containers do not support polymorphism unless you make a base class and store
pointers to base class instances. But, doing so leads potentially to the
above problem with losing type information and also creates tighter coupling
in your code because now two potentially unrelated types must conform to some
virtual interface defined by the common base class.
Introducing
type erasure. To use the above MyVector example (it isn't
a very good one here, but it illustrates the point), what if MyVector didn't
have to expose T as part of its type? Then, I could store a MyVector of ints
in the same container as a MyVector of std::strings without resorting to
derivation and polymorphism.
In fact, boost::any is a good example of type erasure. boost::any allows you
to store absolutely anything inside it, but boost::any itself is not a template
class -- it does not expose the type of what is contained inside it.
boost::function is another example of type erasure.
But what can it do for you? Why bother?
Let's take an RPG game as an example. The game has different kinds of items:
weapons of various types, armor of various types, helmets of various types,
scrolls, magic potions, etc. etc. I want to be able to store all of these
in my backpack. Immediately an STL container comes to mind -- perhaps a deque.
But that means that either I must make one class called Item that is a superset
of attributes of all the different kinds of items, or I must make Item a base
class of all those types. But then, once I've stored the Item in the backpack,
I've lost its real type. If I want to prevent the player from, say, wielding
a scroll as a weapon or donning a flashlight for armor, I must resort to downcasts
to check if the Item is really the right type.
But there is an alternative:
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 ) ) );
}
| |
And now I am able to store objects of disparate types in my backpack.
The cynic will argue that I'm not storing polymorphic types; I'm storing
Objects. Yes ... and no. As we'll see, Object is a simple "passthrough"
object that becomes transparent to the programmer later.
But, you say, you've just done the inheritance thing. How is this better?
It is better not because it affords you more functionality than the inheritance
approach, but because it does not tighly couple Weapons and Armors etc through
a common base class. It gives me the power of retaining type as templates do.
Suppose I want now to look at all items that are capable of doing damage to
an opponent in battle. Well, all Weapons will do that, and perhaps some, but
not all Scrolls and Potions. A scroll of Fire will damage an opponent, a
scroll of Enchant Armor not so much.
Here's one way:
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();
}
| |