Design Patterns with Signature-Based Polymorphism: A C++ Approach, Part 2
- Template Method Pattern Revisited
- Passing the Buck: Chain of Responsibility
- Conclusion
- Notes and References
This series describes an interesting but rarely mentioned technique in a C++ context: signature-based polymorphism (SBP), a more permissive variation of subtype polymorphism [1], usually called duck typing. Two objects having nothing in common can share an implicit interface and be commonly manipulated by such an interface—no inheritance involved. After introducing the key concepts in the first part of this series [2], Part 2 continues the journey by investigating the impact on some well-known patterns such as template, visitor, and chain of responsibility [3]. Standard implementations published or proposed in different contexts will be contrasted with SBP-based ones throughout this article.
Template Method Pattern Revisited
Due to its relative simplicity, this pattern is a good starting point for our discussion. The main intent of the pattern is to create the skeleton of an algorithm by defining its structure (the concept) in a base class, but defer its realization. The pattern uses inheritance to allow subclasses to (re)define the behavior. Like all inheritance-based designs, this solution suffers from a lack of flexibility, coupling the concept with the varying class hierarchy.
Let’s consider the example of an obvious template method:
struct TemplateBase { void operator()() ...{ begin(); end(); } virtual ~TemplateBase(){}; virtual void begin() = 0; virtual void end() = 0; };
But TemplateBase reveals implementation details by exposing unnecessarily the mechanism used by the template method as an interface. Making the details private—sometimes called nonvirtual interface idiom (NVI) [4]—doesn’t help too much from a design perspective. Because of inheritance, begin() and end() are forever tied to the template method, and subsequent classes in the hierarchy lack independence. A more flexible design might use a strategy-based approach:
struct BeginEnd { virtual void begin() = 0; virtual void end() = 0; }; struct TemplateBase { void operator()() { be.begin(); be.end(); } private: BeginEnd be; };
Now any class fulfilling the BeginEnd contract can freely participate in the template method, resulting in a more flexible design.
Back to the initial TemplateBase: we can reimplement the pattern using SBP in a straightforward way, hiding the concept behind the interface (see template.h):
class ITemplateHandler { private: typedef void (*FP1)( void* ); public: template <class T> ITemplateHandler(T& x) : p_(&x), fn_(&functions<T>::dispatchImpl) {} void operator()() { fn_(p_); } private: template <class TT> struct functions { static void dispatchImpl( void* a ) { TT* t = static_cast< TT*>(a); t->begin(); t->end(); } }; void* p_; FP1 fn_; };
The template method defers execution to dispatchImpl, which actually hides the structure of the "algorithm." The concept supported in this example is any class exposing begin() and end() methods. Assuming that Handler1 is such a class, we can write the following:
Handler1 h1; ITemplateHandler th(h1); th();
What we achieved is the flexibility of using any class implementing the concept without requiring any inheritance relationship. Taking into account that ITemplateHandler is not a trivial class and reimplementing it for other concepts might prove tedious, we might want to improve the design and reuse the boilerplate as much as possible.
There are two possible solutions. The first one keeps the concept external to the interface via an additional template, like this:
template <class CONCEPT> class ITemplateHandlerConcept { //same as previous example private: template <class TT> struct functions { static void dispatchImpl( void* a ) { TT* t = static_cast< TT*>(a); CONCEPT()(t); } }; //.... };
The concept is nothing more than the template method wrapped for convenience in a functor and externalized:
struct TemplateConcept { template <class T> void operator()(const T* t) const { t->begin() ; t->end(); } };
Now we can write code like this, where h1 and h2 are different concept implementations:
ITemplateHandlerConcept<TemplateConcept> thc(h1); thc(); ITemplateHandlerConcept<OtherTemplateConcept> thc1(h2); thc1();
ITemplateHandlerConcept is now 100 percent reusable, and there is no inheritance at all. ITemplateHandlerConcept is bound to a certain concept at compile time in a flexible way that probably fits most of the bills.
The same effect can be achieved using the idiom curiously called "The Curiously Recurring Template Pattern (CRTP)"[5]. CRTP enables you to factor out common behavior, and coupled with subtype polymorphism creates a very powerful tool:
template <class TEMPLATE_IMPL> struct TemplateBase { private: typedef void (*FP1)(const void*, const void* ); public: template <class T> TemplateBase( T& t) : p_(&t), fn_(&functions<T>::processImpl) {} void operator()() const { assert(p_); assert(fn_); fn_(this, p_); } private: template <class TT> struct functions { static void processImpl(const void* a, const void* p ) { TT* t = static_cast< TT*>(p); ( *static_cast< TEMPLATE_CONCEPT*>(a) )(t); } }; void* p_; FP1 fn_; }; struct TemplateHandler : TemplateBase<TemplateHandler> { template <class T> void operator()(const T* t) const { t->begin() ; t->end(); } };
The usage is straightforward:
TemplateBase<TemplateConcept> tb1(h1); tb1();
CRTP implementation is similar to the canonical Template Method, while ITemplateHandlerConcept is more in line with the strategy-based implementation. CRTP doesn’t bring much value in this case, but sometimes it adds more precision to the design.
And now the final touch: reapplying SBP to further hide the type of the handler:
class ITemplate { private: typedef void (*FP1)( void* ); public: template <class T> ITemplate(T& x) : p_(&x), fn_(&functions<T>::dispatchImpl) {} void operator()() { fn_(p_); } private: template <class TT> struct functions { static void dispatchImpl( void* a ) { ( *static_cast< TT*>(a) )(); } }; void* p_; FP1 fn_; };
ITemplate is a fully reusable class that has only one role: to offer a unique, nonvariable interface to all types implementing the function operator. This enables seamless manipulation of nonhomogeneous collections or unrelated types, for example:
ITemplate i(tb1); //tb1 is TemplateBase<TemplateConcept> i(); i = thc; //thc is ITemplateHandlerConcept<TemplateConcept2> i();