More about Dynamic and Static Polymorphism
In my last post, “Dynamic and Static Polymorphism“, I introduced dynamic polymorphism. Today, I continue with static polymorphism and present a very interesting idiom in C++: curiously recurring template pattern (CRTP).
A short recap. This is where I left in my last post.
Dynamic Polymorphism is based on object orientation and enables us to separate between the interface and the implementation of a class hierarchy. To get late dynamic dispatch, you need two ingredients: virtuality and an indirection such as a pointer or a reference. The following program exemplified dynamic polymorphism:
// dispatchDynamicPolymorphism.cpp
#include <chrono> #include <iostream> auto start = std::chrono::steady_clock::now(); void writeElapsedTime(){ auto now = std::chrono::steady_clock::now(); std::chrono::duration<double> diff = now - start; std::cerr << diff.count() << " sec. elapsed: "; } struct MessageSeverity{ virtual void writeMessage() const { std::cerr << "unexpected" << '\n'; } }; struct MessageInformation: MessageSeverity{ void writeMessage() const override { std::cerr << "information" << '\n'; } }; struct MessageWarning: MessageSeverity{ void writeMessage() const override { std::cerr << "warning" << '\n'; } }; struct MessageFatal: MessageSeverity{}; void writeMessageReference(const MessageSeverity& messServer){ // (1) writeElapsedTime(); messServer.writeMessage(); } void writeMessagePointer(const MessageSeverity* messServer){ // (2) writeElapsedTime(); messServer->writeMessage(); } int main(){ std::cout << '\n'; MessageInformation messInfo; MessageWarning messWarn; MessageFatal messFatal; MessageSeverity& messRef1 = messInfo; MessageSeverity& messRef2 = messWarn; MessageSeverity& messRef3 = messFatal; writeMessageReference(messRef1); writeMessageReference(messRef2); writeMessageReference(messRef3); std::cerr << '\n'; MessageSeverity* messPoin1 = new MessageInformation; MessageSeverity* messPoin2 = new MessageWarning; MessageSeverity* messPoin3 = new MessageFatal; writeMessagePointer(messPoin1); writeMessagePointer(messPoin2); writeMessagePointer(messPoin3); std::cout << '\n'; }
Static polymorphism is based on templates. Refactor the program using the Curiously Recurring Template Pattern (CRTP).
Static Polymorphism
Before I refactor the previous program dispatchDynamicPolymorphism.cpp,
here is the key idea of CRTP: A class Derived
derives from a class template Base
and Base
has Derived
as a template argument.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
template <typename T> class Base { ... }; class Derived : public Base<Derived> { ... };
Here is the pure nature of CRTP:
// crtp.cpp #include <iostream> template <typename Derived> struct Base{ void interface(){ // (2) static_cast<Derived*>(this)->implementation(); } void implementation(){ // (3) std::cout << "Implementation Base" << std::endl; } }; struct Derived1: Base<Derived1>{ void implementation(){ std::cout << "Implementation Derived1" << std::endl; } }; struct Derived2: Base<Derived2>{ void implementation(){ std::cout << "Implementation Derived2" << std::endl; } }; struct Derived3: Base<Derived3>{}; // (4) template <typename T> // (1) void execute(T& base){ base.interface(); } int main(){ std::cout << '\n'; Derived1 d1; execute(d1); Derived2 d2; execute(d2); Derived3 d3; execute(d3); std::cout << '\n'; }
I use in the function template execute
(line 1) static polymorphism. Each base invoked the method base.interface
. The member function Base::interface
(line 2) is the key point of the CRTP idiom. The member function dispatches to the implementation of the derived class: static_cast<Derived*>(this)->implementation(). That is possible because the method will be instantiated when called. At this point in time the derived classes Derived1, Derived2
, and Derived3 are fully defined. Therefore, the method Base::interface can use the implementation of its derived classes. Pretty interesting is the member function Base::implementation (line 3). This function plays the role of a default implementation for the static polymorphism for the class Derived3
(line 4).
Here is the output of the program:
Now, let me take the next step and refactor the program dispatchDynamicPolymorphism.cpp.
// dispatchStaticPolymorphism.cpp #include <chrono> #include <iostream> auto start = std::chrono::steady_clock::now(); void writeElapsedTime(){ auto now = std::chrono::steady_clock::now(); std::chrono::duration<double> diff = now - start; std::cerr << diff.count() << " sec. elapsed: "; } template <typename ConcreteMessage> // (1) struct MessageSeverity{ void writeMessage(){ // (2) static_cast<ConcreteMessage*>(this)->writeMessageImplementation(); } void writeMessageImplementation() const { std::cerr << "unexpected" << std::endl; } }; struct MessageInformation: MessageSeverity<MessageInformation>{ void writeMessageImplementation() const { // (3) std::cerr << "information" << std::endl; } }; struct MessageWarning: MessageSeverity<MessageWarning>{ void writeMessageImplementation() const { // (4) std::cerr << "warning" << std::endl; } }; struct MessageFatal: MessageSeverity<MessageFatal>{}; // (5) template <typename T> void writeMessage(T& messServer){ writeElapsedTime(); messServer.writeMessage(); // (6) } int main(){ std::cout << std::endl; MessageInformation messInfo; writeMessage(messInfo); MessageWarning messWarn; writeMessage(messWarn); MessageFatal messFatal; writeMessage(messFatal); std::cout << std::endl; }
In this case, all concrete classes (lines 3, 4, and 5) derive from the base class MessageSeverity
. The member function writeMessage
is the interface that dispatches to the concrete implementations writeMessageImplementation
. To achieve this, the object will be upcasted to the ConcreteMessage: static_cast<ConcreteMessage*>(this)->writeMessageImplementation();
. This is the static dispatch at compile time and coined the name for this technique: static polymorphism.
It took me time to get used to it, but applying the static polymorphism in line (6) is quite easy.
In the end, I want to compare dynamic and static polymorphism in a few words:
Dynamic Versus Static Polymorphism
Dynamic polymorphism happens at run time, and static polymorphism at compile time. Dynamic polymorphism typically requires a pointer indirection at run time (read the post “Demystifying virtual functions, Vtable, and VPTR in C++“), but static polymorphism has no performance costs at run time. Admittedly, there is a reason why the idiom curiously recurring template pattern (CRTP) has the name curious inside. For beginners, the idiom is quite challenging to understand. So, what should you use?
First of all, don’t overestimate the costs of a virtual dispatch. In most cases, you can ignore them. Read the excellent paper “Technical Report on C++ Performance” for details. It’s pretty dated but has in section 5.3.3 exciting numbers about the additional costs of virtual function calls. If you are still concerned about performance, there is only one cure: measure. Put your performance tests under version control. Always rerun them if something in your setup consisting of your hardware, compiler, or compiler version changes because this invalidates your previous performance numbers.
In the end, code is way more often read the written. Therefore, you should use the techniques your team is most comfortable with.
What’s next?
Mixins are a widespread technique in Python. They allow you to change the behavior of a class using multiple inheritances. Thanks to CRTP, we also have mixins in C++. Read about them in my next post.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!