Saturday, December 22, 2007

You've got your Templates in my Inheritance

Templates and object-oriented inheritance are the two main features that separate C++ from it's precursor C. The can both be used to solve complex problems on their own, but when used together some truly unique properties pop out. One of the most useful is static polymorphism.

Polymorphism is the reason virtual methods exist. The ability to separate interface from implementation can reduce program complexity greatly; but at a cost. Every type that uses virtual methods must have a vtable, and every object of that type must have a pointer to this vtable. In and of itself this isn't a huge cost. However, a poorly chosen object hierarchy can introduce hidden bloat.

I've seen projects were more then a meg of memory was wasted on these hidden vtables. This might not sound like a lot, but on an embedded system it matters.

There is also a runtime cost, as the method pointer is looked up in the v-table. With modern CPU caching this isn't really a big deal, but again, embedded system caches are usually very small.

There is a little trick that can eliminate these problems, with some caveats. It uses the curiously recurring template pattern.

Here's a class "A" that calls dispatches foo to whatever its template argument is.
template < class D >
class A
{
public:
void foo()
{
static_cast<D*>(this)->foo();
}
};
Here's two sub-classes of A, B & C.

class B : public A<B>
{
public:
void foo()
{
printf("B::foon");
}
};

class C : public A<C>
{
public:
void foo()
{
printf("C::foon");
}
};
Notice how both these classes supply themselves as the template argument for A.

Here's a function which calls foo on its argument.

template< class D >
void call_foo( A<D>& a )
{
a.foo();
}
Now when you run call_foo, on an object of type B or C, you'll get polymorphic behavior. Try it.

int main()
{
B b;
C c;

call_foo( b );
call_foo( c );

return 0;
}
This will effectively give you polymorphism without a v-table and without the run-time dispatch overhead. There are some caveats though. Because templates are evaluated at compile time, the compiler has to infer exactly what type something is so it knows which version of foo is invoked. This prevents doing things like putting B's and C's into the same collection.

Overall it's a handy trick that's used in many modern C++ api's. For example Microsoft's WFC API & Boost.



No comments: