[C++] Static, Dynamic Polymorphism, CRTP and C++20’s Concepts

Let's talk about Polymorphism, CRTP as a way to use static polymorphism and how C++20 can change the way how we write code.

Dynamic Polymorphism

When it comes to interfaces, we think of a base class and a concrete implementation. With dynamic polymorphism we can write this code:

// base class conatains a virtual function
struct base {
    virtual void do_work() = 0;
};

// which we override, to provide a concrete implementation 
struct derived : public base {
    void do_work() override {
        // let's do some work here ... 
    }
};

In general, there's nothing wrong to do so in the first place, but in a C++ context there are some comments here.

First, since we use virtual functions we create a vtable and during runtime we call the apropriate function. There are benchmarks and articles which show that this has impact on the applications performance.

Second, do we really need to take the concrete implementation during runtime? In my experience it's more often the case that I already know during compile time which one I need. I'm often confused why I use something dynamic when I don't need it dynamic.

 

Static Polymorphism

Let's move the implementation from above to a static one. CRTP (Curiously recurring template pattern) is a method which can be seen as static polymorphism (note, there are other advantages to use CRTP it's not that CRTP is static polymorphism).

// now we have a templated base class
template<typename T> 
struct base {
    // no virtual function here
    void do_work () {
        // and we cast this to the template type, where the actual implementation lives
        static_cast<T*>(this)->do_work_impl();
    } 
};

// our class derived inherits from base and passes itself as template parameter
class derived : public base<derived> {
    // declare base as friend so do_work_impl can be called
    // because we still want to call the base class function
    friend class base<derived>;
    void do_work_impl() {
        // let's do some work here ... 
    }
};

We'll make sure that base is used that way and therefore with static_cast<T*>(this) we have access to the actual concrete implementation. Now we have a static approach of an interface.

... But:

  • It's not so clear what methods needs to be implemented (compared to virtual ones)
  • We need to rename the functions on the concrete implementation
 

C++20's Concepts

Let's move on and introduce concepts, which are basically named constraints in our code. Some general examples of concepts are:

// we can assign a condition to a concept
// in this case wheter my given type is an integral type or not
template<typename T>
concept is_numeric = std::is_integral_v<T>;


// there are different ways to apply the concept

//1. use requires before the function
template<typename T>
requires is_numeric<T>
void foo(T t) { 
    //...
}

//2. use requires after the function
template<typename T>
void foo(T t) requires is_numeric<T> { 
    //...
}

//3. use the concept as template argument
template<is_numeric T>
void foo(T t) {
    // ...
}

//4. use the concept in combination with auto as argument
void foo(is_numeric auto t) {
    // ...
} 

And we also can have sort of code as concept, which is evaluated at compile time. Consider a function which needs to increment (post and pre) a variable. You can write concepts like this:

template<typename T>
// introduce requires keyword where we pass a type T in
concept has_increment = requires(T t) 
// all the code is validated at compile time
// if there would be errors, this would not compile
{
    t++;
    ++t;
};


template<has_increment T>
void foo(T t){
    // ... 
}

struct bar {};

//...

foo(1); // ok
foo(bar{}); // error

And we have another benefit here, which is clear error messages. With the last example we'd get this error:

<source>:91:9:   required for the satisfaction of 'has_increment<T>' [with T = baz]
<source>:91:25:   in requirements with 'T t' [with T = baz]
<source>:92:6: note: the required expression '(t ++)' is invalid
   92 |     t++;
      |     ~^~
<source>:93:5: note: the required expression '++ t' is invalid
   93 |     ++t;
      |     ^~~
 

And now we can apply concepts to create an interface for our base class where we want to implement do_work():

// we create a concept can_work to check if do_work is implemented
// this will describe our interface
template <typename T>
concept can_work = requires(T t) {
    t.do_work();
};

// now we apply this concept to an empty type which represents a worker (or our base class)
template<can_work T>
struct worker : public T {};

// now create a concrete worker (corresponding derived) where we implement do_work
struct concrete_worker {
    void do_work() {
        // ...
    }
};

// nice to have: an alias for our concrete worker
using my_worker = worker<concrete_worker>;

//...
// which we can use now
my_worker w;
w.do_work();

And there we have it, we expressed our interface in the concept and on the concrete implementation we removed the inheritance. With the final alias we can then use the concrete type we want.

 

Conclusion

I really like the idea of concepts. This will affect the way we used to write code, especially generic code. It makes it easier to write and to read. Also the error messages way better. Compared to templates, which make it often hard to understand and comprehend this can be a time safer.

But that's it for now. Start using concepts.

Best Thomas

 

Edit

Of course we can apply the concept directly on a worker. Then we'd need to evaluate our worker, like this:

template <typename T>
concept can_work = requires(T t) {
    t.do_work();
};

struct worker {
    void do_work() {
        // ...
    }
};
static_assert(can_work<worker>);
Previous
Previous

[C++] A Boost Asio Server-Client Example

Next
Next

[TypeScript] VS Code API: Let’s Create A Tree-View (Part 3)