- Signature-Based Polymorphism and Interfaces
- External Polymorphism
- Signature-Based Interface
- Variations on an Interface Theme
- Conclusion
- Notes and References
Variations on an Interface Theme
1. A Strategy for Embedded Environments
There are embedded environments in which global data and/or virtual mechanisms are not allowed (see note [10]). The lack of support virtual functions imposes signature-based interfaces as the only possible alternative. Disallowing global data means that we have to retrofit the inner structure and replace the static member with something different. One option is the following (refer to file 3.h):
struct Strategy1 { void f() { std::cout << "strategy 1" << std::endl; } }; struct Strategy2 { void g() { std::cout << "strategy 2" << std::endl; } }; template <class T> void adapt( T* t) { t->f(); } template <> void adapt<Strategy2>( Strategy2* t) { t->g(); } struct IStrategy { template<class T> IStrategy(T* x) : x_(x), fun_(vTable_<T>::f) {} void f() const { fun_(x_); } private: typedef void (*FUN)(void*); template <class TT> struct vTable_ { static void f(void* x) { adapt(static_cast<TT*>(x)); } }; private: FUN fun_; void* x_; }; struct Context { Context(const IStrategy& strategy) : strategy_(strategy) {} void f() const { strategy_.f(); } private: const IStrategy& strategy_; }; void test() { Strategy1 s1; Strategy2 s2; IStrategy i(&s1); Context c(i); c.f(); i = &s2; c.f(); }
The above example demonstrates three different techniques:
- Replacing the static member vtable inside the inner struct with an external pointer to function—FUN fun_—that takes directly the function address(es) from vtable. This is a fair trade-off between size and simplicity for small interfaces.
- Adapting a non-conformant function (g()) by using an external, easy-to-specialize adapter.
- How Strategy pattern can be implemented using interfaces. An interface is used directly in places in which subclasses are used otherwise (in OOP, Context would have been initialized with a subclass implementing the ABC IStrategy; here IStrategy is used directly).
2. Hocus-Pocus with Interfaces
What follows is a more complex example of what is possible to do using generic programming techniques, namely SFINAE. The SFINAE (substitution-failure-is-not-an-error) principle (see note [11]) states that if an invalid argument type or return type is formed during the instantiation of a function template, the instantiation is removed from the overload resolution set instead of causing a compilation error. This, among other consequences, allows enabling/disabling methods in an interface based on external criteria, virtually creating the concept of an interface with variable number of methods.
Why might this be needed? Imagine Tst_2 in example 1.h having f() only. The compilation would fail at this line as there is no function g() defined:
static_cast<TT*>(x)->g();
To allow the same interface to respond to f() and/or g() calls, a more permissive interface is needed—and this is presented in the following example (refer to file 4.h).
Two supplementary files were provided in the accompanying code (refer to files 2.h and 5.h), demonstrating step by step for the curious reader how this concept evolved.
The wizardry is based on the enable_if_1 family of classes (the same as boost::enable_if—see note[12]) that makes a function like the following to be instantiated as long as T has a T::type:
template <class T> typename enable_if_1<I & FUN_F, T>::type f()
Some other changes were necessary:
- The concept-classes take an enabled_fun member that describes (in a crude way) what functions will be used from the interface via a bitset.
- Methods have to be invoked using the return type as a template parameter (refer to file 5.h for other options).
- The suppression mechanism has to be installed at three different levels: interface, constructor, and vtable. Additional set_X helper functions were required to properly resolve the constructor level. A special note for the suppression of interface methods. As no type information is available at this level, a tradeoff is imposed: either define the largest scope of the interface (the maximum number of methods supported) and the methods that are not supported will throw an exception at run-time, or you define the exact number of methods and those methods will be eliminated via SFINAE, resulting in compile time errors. For example:
Tst_1 t1; typedef Interface<FUN_F | FUN_G> Interface; Interface i(&t1); i.f<Interface::FUN_R>(); i.g<void>(); i.g() will throw an exception. For: typedef Interface<FUN_F> Interface; i.g() will generate a compile time error.
- The whole implementation can be improved and made much leaner using macros, of course, but this is beyond the scope of this article.
enum FUN {FUN_F = 1, FUN_G = 4}; struct Tst_2 { const static int enabled_fun = FUN_F | FUN_G; void f() { std::cout << "f" << std::endl; } void g() { std::cout << "g" << std::endl; } }; struct Tst_1 { const static int enabled_fun = FUN_F; void f() { std::cout << "f" << std::endl; } }; template <int I, class T> struct enable_if_1 {typedef T type;}; template < class T> struct enable_if_1<0, T > {}; template <int I, class T> struct Int2Type {typedef T type;}; template <class T> struct Int2Type<0, T> {typedef void type;}; template <int I> struct Interface { typedef void FUN_R; template<class T> Interface(T* x) : x_(x), f_(0), g_(0) { set_g( (typename Int2Type<T::enabled_fun&FUN_G, T>::type*)0); set_f( (typename Int2Type<T::enabled_fun&FUN_F, T>::type*)0); } template <class T> void set_f (T* p = 0) { f_ = (vTable_<T>::template f<void>); } void set_f (void* v =0) {} template <class T> void set_g (T* p = 0) { g_ = (vTable_<T>::template g<void>); } void set_g (void* v =0) {} template <class T> typename enable_if_1<I & FUN_F, T>::type f() { f_ ? f_(x_) : throw; } template <class T> typename enable_if_1<I & FUN_G, T>::type g() { g_ ? g_(x_) : throw ; } private: template <class TT> struct vTable_ { template<class T> static typename enable_if_1<TT::enabled_fun & FUN_F, T>::type f(void* x) { static_cast<TT*>(x)->f(); } template<class T> static typename enable_if_1<TT::enabled_fun & FUN_G, T>::type g(void* x) { static_cast<TT*>(x)->g(); } }; private: typedef void (*FUN)(void*); FUN f_, g_; void* x_; }; void test() { { Tst_2 t2; typedef Interface<FUN_F | FUN_G> Interface; Interface i(&t2); i.f<Interface::FUN_R>(); i.g<Interface::FUN_R>(); } { Tst_1 t1; typedef Interface<FUN_F | FUN_G> Interface; Interface i(&t1); i.f<Interface::FUN_R>(); i.g<void>(); } }
Interface<FUN_F | FUN_G> represents an Interface servicing both f(0 and g(), while Interface<FUN_F > exposes f() only. If used like this, they represent two different types that cannot be mixed together. On the other hand, it is perfectly possible to use the Interface with the broadest scope (Interface<FUN_F | FUN_G>) everywhere and to detect at run-time this time the failure of instantiating g_ as in the above example.