Introduction to CRTP / Facade#
When perusing the existing POLARIS source code, you will likely find a peculiar way of structuring classes which is used throughout the code. The design uses a combination of CRTP (Curiously Recurring Template Pattern) and the Facade Pattern. See a description of these concepts here. Like all the other style guidelines, following this pattern is optional, but we have found it useful and recommend it where appropriate for developing extensible and modular code.
Layout#
A conventional interface looks something like this:
struct Agent
{
virtual void Do_Stuff() = 0;
};
struct Agent_Base : public Agent
{
virtual void Do_Stuff(){}
};
And client code using it might look something like this:
void Client_Do_Stuff(Agent* bob)
{
bob->Do_Stuff();
}
A CRTP/Facade implementation looks like this:
template<typename ComponentType>
struct Agent
{
void Do_Stuff(){ static_cast<ComponentType*>(this)->Do_Stuff; }
};
struct Agent_Implementation : public Agent<Agent_Implementation>
{
void Do_Stuff(){}
};
And client code using it might look something like this:
void Client_Do_Stuff(agent_type* bob)
{
bob->Do_Stuff();
}
The key idea here is that agent_type may be defined far away from where it is used (usually in MasterType). It serves as a qualitative identifier for an object type which gets disseminated throughout the code base.
Analysis#
The prototype-implementation pattern is not superior to the traditional abstract interface pattern, it simply accomplishes similar objectives and has different strengths and weaknesses.
Performance#
The main difference between “conventional” polymorphism and the CRTP pattern is the performance gain in compile-time polymorphism versus run-time-polymorphism.
In run-time polymorphism each time a virtual functions is called, the processor must do a look up in a virtual table and load the correct function pointer for the class and call that function. This hurts performance in two ways. The first is direct - looking up a memory location and calling a function pointer is slower than simply calling a function whose location is known. The second is indirect - each time a virtual function is encountered it must be determined where the execution code is located and load it into the execution cache for the CPU. It can take quite a few machine cycles to locate and load that code.
These overheads can be relatively minor with modern CPU caching technologies, however the “fastest possible” code is not obtainable with heavy virtual function use in bottleneck code sections.
The prototype-implementation pattern is able to resolve everything at compile time meaning that no performance overhead is incurred.
Changing an Underlying Type#
The key advantage both virtual and prototype-implementation design bring is that they make code resistant to the underlying type being changed.
In this example, if we wanted to change Agent_Base to Other_Agent_Base the client doesn’t need to do anything.
struct Agent
{
virtual void Do_Stuff() = 0;
};
struct Agent_Base : public Agent
{
virtual void Do_Stuff(){}
};
struct Other_Agent_Base : public Agent
{
virtual void Do_Stuff(){}
};
void Client_Do_Stuff(Agent* bob)
{
bob->Do_Stuff();
}
int main()
{
//Agent_Base bob;
Other_Agent_Base bob;
Client_Do_Stuff((Agent*)&bob);
}
The prototype-implementation pattern gives a similar advantage by re-writing the global definition of agent_type in MasterType:
template<typename ComponentType>
struct Agent
{
//Note that the function called from the implementation is
//different from the prototype - this prevents a circular call
void Do_Stuff(){ static_cast<ComponentType*>(this)->Do_Stuff_impl(); }
};
struct Agent_Implementation : public Agent<Agent_Implementation>
{
void Do_Stuff_impl(){}
};
struct Other_Agent_Implementation : public Agent<Other_Agent_Implementation>
{
void Do_Stuff_impl(){}
};
struct MasterType
{
//typedef Agent_Implementation agent_type;
typedef Other_Agent_Implementation agent_type;
};
void Client_Do_Stuff(MasterType::agent_type* bob)
{
// Call the prototype name (Facade pattern)
bob->Do_Stuff();
}
int main()
{
//Agent_Implementation bob;
// or
//Other_Agent_Implementation bob;
//Can use type defined in MasterType
MasterType::agent_type bob;
Client_Do_Stuff(&bob);
}
Notice that this is not quite as good as virtual for certain situations and if you wish to have heterogeneous agents, it doesn’t directly enable that strategy. However, it is significantly more resilient than simply encapsulating a private member in the public class space.
POLARIS Component Development#
Using this design pattern requires a fair bit of typing and care must be taken to do it correctly. We have developed macros to help with this. The macros also make it easier to read. See Building Your First Agent.
Conclusion#
There a number of other reasons why we made heavy use of the prototype-implementation design which may be consulted at: Advanced Implementation Prototype Design.