Reputation: 4954
In order to store CRTP object pointers in a homogenous container the templated base class can itself be derived from a class common_base
that defines a pure virtual interface and (if required) a virtual destructor. This is sometimes referred to as the CRTP pitfall workaround.
class common_base
{
public:
virtual ~common_base() {}
};
template<typename T> class base : public common_base {
public:
void func() {
printf("base func()\n");
static_cast<T*>(this)->func();
}
};
class derived : public base<derived> {
public:
void func() {
printf("derived func()\n");
}
};
derived d;
base<derived>& b = d;
d.func(); // Output: derived func()
b.func(); // Output: base func()
// derived func()
All good. Now let's say I want to use the workaround to not only store derived
objects in a container, but to also call a common interface on those objects. So I change common_base
to be:
class common_base
{
public:
virtual ~common_base() {}
virtual void func() = 0;
};
derived d;
base<derived>& b = d;
common_base& c = d;
d.func(); // Output: derived func()
b.func(); // Output: derived func()
c.func(); // Output: derived func()
The above pure virtual function pattern is present in references I've seen but it looks like it causes a vtable to be used in resolving the func()
calls at runtime, which negates the performance benefit of the compile time polymorphism of CRTP. Is it not possible to declare a common base interface with CRTP early binding in this way?
References:
Modern C++ Programming Cookbook - Second Edition
Upvotes: 1
Views: 149
Reputation: 119877
CRTP (or anything else for that matter) is not a way to have benefits of late binding without actually having late bindings. If late bindings were replaceable by a better mechanism, they would have been replaced. They have not, and they are not.
CRTP is a way of having common code base (living in a template) without having a common base class (because templates are not classes). Absence of a common base class is the whole point. Since there is no common base class, the dynamic type is either the same as the static type or is embedded in the static type as a template parameter, so it's always statically known, and no dynamic dispatch is needed. You cannot reintroduce a common base class into the picture and still use static dispatch everywhere. That would require a miracle, and miracles are forbidden in C++.
So does this "CRTP pitfall workaround" actually achieve anything? Let's have a closer look. The book referenced in the question shows an example:
class controlbase
{
public:
virtual void draw() = 0;
virtual ~controlbase() {}
};
template <class T>
class control : public controlbase
{
public:
virtual void draw() override
{
static_cast<T*>(this)->erase_background();
static_cast<T*>(this)->paint();
}
};
Derived classes like class button : public control<button>
implement erase_background
and paint
.
In this setting, draw
is called with the dynamic dispatch mechanism, while erase_background
and paint
are dispatched statically. (There is no contradiction with the above: only draw
belongs to the common base class, and calls to draw
need to be dynamically dispatched; other calls are not made to methods of the common base class, and those can be in principle statically dispatched). So there's a net win: all calls but one are effectively devirtualised. On the other hand, a competent optimiser should in principle be able to always devirtualise those other calls without the programmer having to resort to CRTP, although I don't know of a compiler that is able to do it.
Upvotes: 4