In a previous issue of Overload [Goldthwaite], Lois Goldthwaite gave an illuminating explanation of compile time polymorphism using templates.
This has the advantage over run time polymorphism that it does not require classes to be derived from a common base class to share an interface, and that it does not incur the cost of a virtual function table.
It is usually pointed out that it suffers the disadvantage that objects of different types with the same interface cannot be held in a type safe container, since such containers do require contained objects to share a common type.
However we can in fact get the best of both worlds since it is possible to convert objects that share compile time polymorphism into objects that share run time polymorphism. I infer from Kevlin Henney's article in Overload 48 [Henney] that the technique I describe uses the External Polymorphism pattern.
First, a recapitulation of Lois' classes:
// talkers.h (include guards not shown) #include <iostream> class Dog { public: void talk() const { std::cout << "woof woof" << std::endl; } }; class CuckooClock { public: void talk() const { std::cout << "cuckoo cuckoo" << std::endl; } }; class BigBenClock { public: void talk() const { std::cout << "take a long tea-break" << std::endl; } void playBongs() const { std::cout << "bing bong bing bong" << std::endl; } }; class SilentClock { };
We can provide compile time polymorphism by means of a function template, which can be specialised to adapt functionality (in the case of BigBenClock) and to supply missing functionality (in the case of SilentClock):
// talkative_generic.h // (include guards not shown) class BigBenClock; class SilentClock; template< class T > void talkativeGenericTalk(const T& t) { t.talk(); } template<> void talkativeGenericTalk(const BigBenClock& bigBenClock); template<> void talkativeGenericTalk(const SilentClock& silentClock); // talkative_generic.cpp #include "talkative_generic.h" #include "talkers.h" #include <iostream> template<> void talkativeGenericTalk(const BigBenClock& bigBenClock) { bigBenClock.playBongs(); } template<> void talkativeGenericTalk(const SilentClock& silentClock) { std::cout << "tick tock" << std::endl; }
The equivalent interface in run time polymorphism can be defined as:
// talkative_interface.h // (include guards not shown) class TalkativeInterface { public: virtual TalkativeInterface* clone() const = 0; virtual void talk() const = 0; virtual ~TalkativeInterface(){} };
We then need a mechanism for converting objects with the compile time interface to this type. We can do this by means of a factory class:
// talkative_interface_factory.h // (include guards not shown) #include "talkative_interface.h" #include "talkative_generic.h" class TalkativeInterfaceFactory { public: // Callers should take ownership of the // pointers returned by the public functions template< class T > static TalkativeInterface* convert(T* t) { return new TalkativeImplementation<T>(t); } template< class T > static TalkativeInterface* copy(const T& t) { return new TalkativeImplementation<T>(t); } private: TalkativeInterfaceFactory(); TalkativeInterfaceFactory(const TalkativeInterfaceFactory&); TalkativeInterfaceFactory& operator=( const TalkativeInterfaceFactory&); ~TalkativeInterfaceFactory(); template< class T > class TalkativeImplementation : public TalkativeInterface { public: TalkativeImplementation(T* t) : t_(t), owner_(false) {} TalkativeImplementation(const T& t) : t_(new T(t)), owner_(true) {} virtual TalkativeInterface* clone() const { return new TalkativeImplementation<T>(*t_); } virtual void talk() const { talkativeGenericTalk(*t_); } virtual ~TalkativeImplementation() { if(owner_) delete t_; } private: TalkativeImplementation(const TalkativeImplementation&); TalkativeImplementation& operator=( const TalkativeImplementation&); T* t_; bool owner_; }; };
The key to the factory class is the nested adapter class template TalkativeImplementation in the private part. This is derived from TalkativeInterface to give it the required type. It is a template so that it can be instantiated with any class T that supports the compile time polymorphism; the required member function talk() is implemented by using the function template talkativeGenericTalk().
The factory provides two static public functions convert() and copy() which use the TemplateImplementation class template to perform the conversion (in the former by sharing an existing pointer, in the latter by creating a new, independent one).
For the sake of clarity I have used raw pointers throughout the code. In practice I would use a reference counting smart pointer everywhere there is a raw pointer. This would avoid the need for the owner_ member variable and make the destructor of TalkativeImplementation trivial. For a full discussion of smart pointers see Alexandrescu [Alexandrescu]).
Here is some test code to show the use of the factory to build a vector of talkative objects using copy():
// test_talkative.cpp #include "talkers.h" #include "talkative_interface_factory.h" #include <vector> #include <algorithm> namespace { typedef std::vector<TalkativeInterface*> Talkers; struct Talk { void operator()(const TalkativeInterface* talker) { talker->talk(); } }; template< class T > struct Destroy { void operator()(const T* t) { delete t; } }; } int main(int, char**) { Dog aDog; CuckooClock aCuckooClock; BigBenClock aBigBenClock; SilentClock aSilentClock; Talkers talkers; talkers.push_back( TalkativeInterfaceFactory::copy(aDog)); talkers.push_back( TalkativeInterfaceFactory::copy( aCuckooClock)); talkers.push_back( TalkativeInterfaceFactory::copy( aBigBenClock)); talkers.push_back( TalkativeInterfaceFactory::copy( aSilentClock)); std::for_each(talkers.begin(), talkers.end(), Talk()); std::for_each(talkers.begin(), talkers.end(), Destroy<TalkativeInterface>()); return 0; }
This produces the output:
woof woof cuckoo cuckoo bing bong bing bong tick tock
One practical application of this factory class would be to implement the Observer pattern [GoF]. In fact, it was Pete Goodliffe's series of articles on that subject [Goodliffe] that set me thinking about this in the first place. In this pattern, a Subject maintains a list of Observers. With this factory class we would be able to convert a variety of Observers, not necessarily sharing a common base class, so that they could be added to such a list. The convert() operation would be used in this case. The following test code demonstrates the effect achieved by using convert() rather than copy():
// guard_dog.h (include guards not shown) #include "talkers.h" #include <iostream> class GuardDog: public Dog { public: GuardDog() : barkLoudly(false) {} GuardDog& deterIntruder() { barkLoudly = true; return *this; } void talk() const { if(barkLoudly) { std::cout << "WOOF WOOF" << std::endl; } else { Dog::talk(); } } private: bool barkLoudly; }; // test_talkative.cpp // other includes as before #include "guard_dog.h" // typedef Talkers, classes Talk and // Destroy as before int main(int, char**) { GuardDog* aGuardDog = new GuardDog(); Talkers talkers; talkers.push_back( TalkativeInterfaceFactory::convert( aGuardDog)); std::for_each(talkers.begin(), talkers.end(), Talk()); aGuardDog->deterIntruder(); std::for_each(talkers.begin(), talkers.end(), Talk()); std::for_each(talkers.begin(), talkers.end(), Destroy<TalkativeInterface>()); delete aGuardDog; return 0; }
This produces the output:
woof woof WOOF WOOF
Overload Journal #49 - Jun 2002 + Programming Topics
Browse in : |
All
> Journals
> Overload
> 49
(8)
All > Topics > Programming (877) Any of these categories - All of these categories |