CRTP is a technique of achieving static polymorphism. And this technique is also called F-bound polymorphism and it is a form of F-bounded quantification. During the first glace, this technique looks like it is a replacement of dynamic polymorphism. However, there are scenarios where it is actually difficult to replace virtual functions. The article will discuss both advantages and disadvantages.
Contrarily, a CRTP class do NOT achieve polymorphic behavior by using inheritance. Instead, member in base specifically calls a member function in derived class.
Dynamic Polymorphism using virtual functions.
First lets revisit an example with, dynamic polymorphism instead. To illustrate, the class Vehicle is a base class and has a virtual function “drive”. Secondly, the derived class, Car will override the “drive” virtual function.
#include <iostream>
using namespace std;
class Vehicle
{
public:
virtual void drive() = 0;
};
class Car : public Vehicle
{
public:
void drive()
{
cout << "Car::drive()" << endl;
}
};
int main()
{
Vehicle *v1 = new Car;
v1->drive();
return 0;
}
Since, v1 is pointing to Car object, therefore, compiler will generate code to call “drive” from the Car class. The output is:
Car::drive()
Same example with CRTP
To understand CRTP, following 2 points are important:
- Firstly, Base class is a template class
- Secondly, Derived class uses itself as template argument to instantiate the base.
The drive member of Vehicle base specifically, calls the drive member of template parameter type. This is because, the template parameter is a very special class which contains the real implementation. In the second place, the derived class knows this understanding of base class. Therefore, it offers itself to become template parameter for base.
#include <iostream>
using namespace std;
template<typename T>
class Vehicle
{
public:
void drive()
{
((T*)this)->drive();
}
};
class Car : public Vehicle<Car>
{
public:
void drive()
{
cout << "Car::drive()" << endl;
}
};
int main()
{
Vehicle<Car> *v1 = new Car;
v1->drive();
return 0;
}
In the above example, v1 is a pointer to template initialization of base class (Vehicle<Car>). However, just like, polymorphism, this points to an object of derived class Car. Therefore, ultimately, drive function from derived class Car is called. And since, no virtual functions were involved, hence the runtime performance is faster.
Therefore, the output is:
Car::drive()
Problem with CRTP
Although CRTP ultimately, calls the member function in derived class. However, the problem is that Base class is actually a template class and not an actual class. Consequently, if there is a second Derived class, named Bus, then base class template initialization will be of different type. This is because, the Bus class will derive from a different specialization, i.e., Vehicle<Bus>
For Example,
class Bus : public Vehicle<Bus>
{
public:
void drive()
{
cout << "Bus::drive()" << endl;
}
};
This means that there will 2 types of Base pointers.
//with CRTP
Vehicle<Car> *v1 = new Car;
Vehicle<Bus> *v2 = new Bus;
Clearly, this has become a different setup than what we had in virtual functions. Because, the dynamic polymorphism gave only one type of Base pointer. But now, with every derived class a different pointer type is available.
//With dynamic polymorphism
Vehicle *v1 = new Car;
Vehicle *v2 = new Bus;
Due to different kinds of It is not possible to store CRTP base class pointers in a vector.
int main()
{
Vehicle<Car> *v1 = new Car;
Vehicle<Bus> *v2 = new Bus;
auto vecObj = {v1, v2}; //This is wrong
return 0;
}
The compiler will generate following error:
crtp.cpp:40:26: error: unable to deduce 'std::initializer_list<auto>' from '{v1, v2}'
40 | auto vecObj = {v1, v2};
| ^
crtp.cpp:40:26: note: deduced conflicting types for parameter 'auto' ('Vehicle<Car>*' and 'Vehicle<Bus>*')
Workaround for vector problem
Workaround is to again fallback to the virtual functions. For example, there may be a top-level abstract class like, AbstractVehicle. Consequently, from this class the CRTP template class Vehicle shall derive.
class AbstractVehicle
{
public:
virtual ~AbstractVehicle() = 0;
};
AbstractVehicle::~AbstractVehicle() {}
template<typename T>
class Vehicle : public AbstractVehicle
{
}
The vector shall be of the type of AbstractVehicle
Vehicle<Car> *v1 = new Car;
Vehicle<Bus> *v2 = new Bus;
std::vector<AbstractVehicle*> vec = {v1, v2}; //Correct