Signature-Based Interface
This approach, first described in note [3], removes the pure virtual functions and uses a hand-crafted dispatch table, giving the compiler the opportunity to optimize the code more than in the previous method. This is the canonical example (see note [9]). Refer to file 1.h:
struct Tst_2 { void f() { std::cout << "f2" << std::endl; } void g() { std::cout << "g2" << std::endl; } }; struct Tst_1 { void f() { std::cout << "f1" << std::endl; } void g() { std::cout << "g1" << std::endl; } }; struct Interface { template<class T> Interface(T* x) : x_(x), table_(vTable_<T>::table) {} void f() { table_.f(x_); } void g() { table_.g(x_); } private: struct VTable { void (*f)(void*); void (*g)(void*); }; template <class TT> struct vTable_ { static VTable table; static void f(void* x) { static_cast<TT*>(x)->f(); } static void g(void* x) { static_cast<TT*>(x)->g(); } }; private: VTable table_; void* x_; }; template <class TT> Interface::VTable Interface::vTable_<TT>::table = {&Interface::vTable_<TT>::f, &Interface::vTable_<TT>::g}; void test() { Tst_2 t2; Interface i(&t2); i.f(); i.g(); Tst_1 t1; i = &t1; i.f(); i.g(); }
It might be interesting to notice at the beginning the obvious differences from the previous method:
- There is only one class instead of two implementing the interface.
- The usage is cleaner (no explicit template parameters; no extra classes to be instantiated).
- The external adapter is missing (just for the moment—it will be presented later).
And now the not-so-obvious implementation details:
When designing a class like Interface, the goal is to "hide" the type of the wrapped object, which means that Interface cannot be templatized, and the type has to be passed via constructor to an internal structure mimicking a v-table. This structure practically mediates the calls without being ever explicitly mentioned in the calling process. For example, a call to f() translates into a call to an intermediate Interface member function table that jumps to its static counterpart that is further implemented in terms of static member functions of vTable<T> (and this is where the type is "remembered"). The whole process is fast due to inlining (meaning that the performance penalties are minimal).