DEV Community

Lena
Lena

Posted on

What if I told you, you can make static polymorphism with virtual functions?

Article::Article

Often virtual functions are shown as an example of runtime polymorphism in C++, in opposition with compile time polymorphism (like template, function overload, CRTP, etc.).

Why? Because the dispatch takes place during runtime instead of compile time!

Meme Morphesus What if I told you, you can make static polymorphism with virtual functions

You heard that right! We'll see how and why in this article.

Note that I won't explain how virtual method are implemented by the compiler, because this is not relevant and not even defined by the standard (even if all of the compilers I know use a vtable).

Explanations

Since C++11 there are constexpr functions. These functions can be evaluated at compile time if the arguments are known during the compilation, it can be a constant like 5 or a constexpr variable or the result of a constexpr function. For example this code compiles :

// A simple function, we just added constexpr in the prototype
constexpr int foo(int n)
{
    // The logic is sooooooo complicated
    return n * 2;
}
// A static assert is like a normal assert,
// but it does with with compile time predicate instead of runtime
// (4 * 2) == 8, everything is good
static_assert(foo(4) == 8);
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link

A predicate is a callable that returns a value testable as a boolean

But that code does not :

// Same as above, we added constexpr in the prototype
constexpr int bar(int n)
{
    // The logic is more complicated here, if that's possible
    return n * 3; 
}
// 2 * 3 != 7, we have an error
static_assert(bar(2) == 7); 
// error: static assertion failed
//    | static_assert(bar(2) == 7);
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link

A constexpr function must satisfy some requirements, for example you can't make I/O in a constexpr function. But with each new standard, the requirements are fewer and fewer. Here's the requirement that interests us today:

it must not be virtual (until C++20)

Meme fun begins When you can use C++20

This means now we can make virtual functions constexpr, therefore they can be evaluated at compile time like this:

// The base class
struct Toto
{
    // The constexpr virtual function we will override
    constexpr virtual int triple(int n) const = 0;
};

// The child class
struct Impl: Toto
{
    // Override the method
    constexpr virtual int triple(int n) const override
    {
        // The unexpected logic
        return 3 * n;
    }
};

// Instanciate the implementation
constexpr auto impl = Impl{};
// We need a reference or a pointer, else the dispatch won't work
constexpr const Toto& impl_ref = impl;

constexpr auto a = impl_ref.triple(3);
static_assert(a == 9); 
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link
And because everything is done during compile time, we can say it is static polymorphism.

You can try this code with C++17, it won't compile, but in C++20 it works like a charm!

Enforce it with consteval

meme sanders I am once again taling about a C++20 feature
With C++20 a new keyword arrives : consteval, the names may seem redundant with other keyword, but it has a utility and it is pretty simple : it is like constexpr but it can only be used during the compilation, meaning that its argument and its results are compile time constant expressions.

Here's an example that compiles :

#include <iostream>

// Put the consteval at the same place you would put the constexpr specifier
consteval int do_stuff(int n)
{
    return n + 3;
}

int main()
{
    // It prints 10
    std::cout << do_stuff(7) << std::endl;
}
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link
And one that does not:

#include <iostream>

// Same function
consteval int do_stuff(int n)
{
    return n + 3;
}

int main(int argc, char** argv)
{
    // argc is not know at compile time, this is not good
    std::cout << do_stuff(argc) << std::endl;
}
// main.cpp: In function 'int main(int, char**)':
// main.cpp:12:26: error: 'argc' is not a constant expression
//    12 |     std::cout << do_stuff(argc) << std::endl;
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link

Article::~Article

I showed you some things you can do in C++ 20 with virtual functions, it may not seem intuitive, but it is possible.

I don't have a specific use case in mind where it is useful, but in general, the more you can do during compile time, the better it is, because it can often have better runtime performance (as it has already be performed during the compilation) but more important, the compiler makes more checks, therefore the code is safer.

Also note that it is just a tool like every other feature of the language, and just because you can use it does not mean you should always use it everywhere, you don't have only a hammer, you have a complete toolbox.
Meme grievous, virtual + constexpr/consteval make a fine addition to my collection

Sources

Top comments (0)