Know ‘constexpr’? Here’s ‘consteval’, ‘constinit’

As you may know, and as described in Chapter 4 of the Guide on this site, functions (and variables) may be declared constexpr in Modern C++. Functions declared with this keyword are guaranteed to be able to be called at compile-time, returning a result which can be used where a compile-time constant is needed. Importantly, these functions can also be called at with run-time-defined parameters, with the result of the function calculated at run-time with the same logic.

The new keyword consteval, added in C++20, can be used with function definitions to ensure that evaluation is performed at compile-time only. The restrictions are similar to those for constexpr functions (global state cannot be modified) with the added restriction that only constant parameter values are passed to the function when it is called. The following function definition is a valid consteval function:

consteval auto f(int i) {
    auto n{ 0 };
	for (int i{ 0 }; i != 41; ++i) {
        n += 1;
    }
    return n + i;
}

A few things to note:

  • The consteval keyword is written before the return type
  • An auto return type specifier is used here, this is permitted
  • A single parameter i is used here, this does not need to be declared const
  • Local variables, such as n and the for-loop’s i here, can be defined and used
  • Looping constructs are permitted
  • The return value is allowed to change depending upon parameter value(s)

Lambdas can also be declared with consteval; the keyword is used in place of mutable, and parentheses are required even if empty:

auto l = []()consteval{ return 42; };

Note that variables cannot be declared consteval, this is not the purpose of this keyword, however the result of a consteval function can be assigned to a const, non-const or constexpr variable (possibly declared with auto). (This assignment is guaranteed to not use a function call at run-time.)

New to C++23 (and not implemented by MSVC at the time of writing) is if consteval. Unlike if and if constexpr, this form is not followed by a parenthesised expression, although an else clause is permitted, as is the form if !constexpr. The logic is straightforward, the if consteval block is evaluated if program flow at this point is being evaluated in a compile-time context, otherwise the else block is evaluated. (The logic is reversed for if !consteval.) The following program (tested under Compiler Explorer) demonstrates this:

#include <iostream>

constexpr auto f(int i) {
    return 41 + i;
}

constexpr int g(int i) {
    if consteval {
        return f(i);
    }
    else {
        return f(i + 1);
    }
}

int main() {
    const auto i = g(1);
    std::cout << "i = " << i << '\n';
    auto j = g(1);
    std::cout << "j = " << j << '\n';
}

The if consteval appears within a constexpr function; within a normal function if consteval would never evaluate to true. Only a function declared consteval can be invoked within the if consteval block, while such functions are not permitted within the else block. A constexpr function (as shown here) is valid in either or both parts. The output produced by this program is:

i = 42
j = 43

As can be observed, assigning g(1) to a const (or constexpr) variable calls f(1), while assigning the same to a mutable variable calls f(2). Allowing if consteval to call a stub function for the definition(s) needed at compile-time, while allowing a “full-fat” non-constexpr/non-consteval to be called at run-time, is the expected use case for this. Also, the return type of the two clauses of if consteval is allowed to be different (where declared auto), so this will likely find uses in generic programming.

To conclude this article, the keyword constinit, new to C++20, allows a variable declared thread_local or static to have static initialization. The same would be true if constexpr were used, however this would also imply constant destruction and const-qualification. For example, constinit could be used with a type such as std::shared_ptr<T>, which has a constexpr constructor but not a constexpr destructor.

Other possibilities of assignment to a constinit variable are: constant literals, expressions evaluating to a constant, consteval functions, and certain constexpr functions (those not using paths which call other non-consteval or non-constexpr functions). The following example program demonstrates its use (expect it to take some time to compile):

consteval unsigned long long fib(unsigned n) {
    return (n < 2) ? n : fib(n - 2) + fib(n - 1);
}

int main() {
    static constinit auto fib28 = fib(28);
    return fib28;
}

Most of the time, you should prefer constexpr over constinit, especially since variables which need to be static or thread_local are comparatively uncommon, and constexpr is able to be used with these, too.

Resources:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s