/ C++

consteval in C++17

A common misconception is that constexpr functions are evaluated during compilation and not during runtime. In reality, a constexpr function makes it possible to be evaluated at compile time without guaranteeing it.

The rest of this post motivates why constexpr is sometimes not enough and how the following snippet can be used for a stronger compile-time evaluation guarantee without jeopardizing the ability to pass runtime values:

template <auto V>
static constexpr auto force_consteval = V;

Consider the following stringhash function:

constexpr size_t stringhash(char const* s) 
{
    size_t h = 0;
    while (*s) 
    {
        h = h * 6364136223846793005ULL + *s + 0xda3e39cb94b95bdbULL;
        s++;
    }
    return h;
}

This function can be used to compute string hashes at compile time:

static_assert(stringhash("hello world") != 0);

Without constexpr, we would get:

error: non-constant condition for static assertion
   14 | static_assert(stringhash("hello world") != 0);
      |               ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~

When the compiler is forced to, constexpr functions can be evaluated at compile time. If not forced, there is no guarantee:

size_t test()
{
    return stringhash("hello world");
}

With -O0, neither clang nor gcc bother to evaluate the call at compile time. Fortunately, with optimizations enabled, they actually do:

test():
  movabsq $-9068320177771933951, %rax
  ret

However, this strongly depends on how the constexpr function is written. A small variation, e.g. a recursive implementation, can be enough to change the compiler’s mood:

constexpr size_t stringhash(char const* s) 
{
    if (!*s)
        return 0;
    return stringhash(s + 1) * 6364136223846793005ULL + *s + 0xda3e39cb94b95bdbULL;
}

Now, clang does not feel to be obligated to optimize it anymore, even at -O2 or -O3.

In C++20, we could use consteval to specify that stringhash must always produce a compile time constant expression. Note that this would mean that we cannot use stringhash with runtime values anymore.

One way to force compile time evaluation is to declare a constexpr variable:

size_t test()
{
    constexpr auto h = stringhash("hello world");
    return h;
}

This guarantees that h is initialized by a constant expression, which in turn means compile time evaluation in practice, even for -O0.

Declaring additional constexpr variables for every call makes this quite cumbersome to use. By using variable templates and auto non-type template parameters, there is another, quite elegant solution:

template <auto V>
static constexpr auto force_consteval = V;

Which can be used as:

size_t test()
{
    return force_consteval<stringhash("hello world")>;
}

The reason I like this solution is because it enforces compile-time evaluation in -O0 and -O2 and does not even introduce additional symbols. If force_consteval were not a static variable but rather inline, a function, or a type, then symbols might get emitted to ensure the value has the same address in each translation unit. This would have a negative impact on binary size, especially if many different values are used across the program.

The auto means that all supported non-type template parameters are supported, e.g. any integral type, enum, or pointers. With C++20 we also get floating-point types and literal types (including “user-defined constexpr classes”).

This solution is still a bit verbose. In C++17, it could be hidden behind a macro:

#define STRINGHASH(str) force_consteval<stringhash(str)>

In C++20, we could use custom literals to build a stringhash<"hello world">.

(Title image from pixabay)