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)