More about Dynamic and Static Polymorphism

Contents[Show]

In my last post "Dynamic and Static Polymorphism", I introduced dynamic polymorphism. Today, I continue with static polymorphism and present are very interesting idiom in C++: curiously recurring template pattern (CRTP).

 StaticDynamic

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. Let me 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.

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:

 crtp

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.

To be honest, 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 requires typically 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. For the details, read the excellent paper "Technical Report on C++ Performance". It's pretty dated but has in section 5.3.3 interesting 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 and 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 popular 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, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Evangelos Denaxas, 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, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Ralf Holly, Juan Dent, George Liao, Daniel Ceperley, and Jon T Hess.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, Ralf Abramowitsch, John Nebel, and Mipko.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

Mentoring Program in English

Do you want to stay informed about my mentoring programs? Write to This email address is being protected from spambots. You need JavaScript enabled to view it..

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Bookable (Online)

German

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

New

Contact Me

Modernes C++,

RainerGrimmSmall

 

Comments   

0 #1 Rud Merriam 2022-03-05 01:34
The "overhead" of a virtual function call is misleading when examined by itself as the performance report does.

What code does the virtual call replace and how does the virtual call compare to that call?

A virtual call is a compiler created decision statement. That call is replacing possibly a switch statement or a sequence of "if / else if" statements. The cost of those should be compared to the virtual call.

More directly, the impact on the overall system must be looked at. As has been oft stated, developers are not good a guessing the performance of code.
Quote
0 #2 Zole 2022-03-14 22:16
Whole code is kind of bad example for polymorphism, as in //dispatchStaticPolymorphism.cpp MessageSeverity struct code is not needed at all, and you can't use a list of MessageSeverity objects that when called writeMessageImplementation would call correct implementation as you can't have a list of MessageSeverity objects as this objects need to be template objects ....
Quote
0 #3 learnedSloth 2022-03-23 10:01
Quoting Rud Merriam:

A virtual call is a compiler created decision statement. That call is replacing possibly a switch statement or a sequence of "if / else if" statements. The cost of those should be compared to the virtual call.


I suppose there's no branch prediction for virtual calls, so it would probably depend on the predictability of those paths.
Quote
0 #4 Paul Fee 2022-04-07 16:47
The link to the "last post" at the top page leads to the top of your website, rather than the previous article.

It should be:
http://www.modernescpp.com/index.php/dynamic-and-static-polymorphism
Quote
0 #5 Rainer 2022-05-10 16:57
Quoting Paul Fee:
The link to the "last post" at the top page leads to the top of your website, rather than the previous article.

It should be:
http://www.modernescpp.com/index.php/dynamic-and-static-polymorphism

Thanks. Fixed
Quote

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Interactive Course: The All-in-One Guide to C++20

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 834

Yesterday 6750

Week 15171

Month 236007

All 9709639

Currently are 139 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments