consteval and constinit

constexpr of lesson 122 allows both: a call may happen in the compiler or at run time. C++20 split the two cases into keywords that cannot be confused, consteval for compile time only and constinit for an initialisation that must not run code at startup.

The first is about a function, the second about a variable. Both turn a rule that used to live in a comment into a compile error, which is the theme of this part of the course.

The static initialisation order problem appears when one global's constructor reads another global that has not been constructed yet. constinit removes the constructor from the picture, and a constexpr global removes the variable from the dynamic phase entirely.

The two keywords are also documentation. A reader of the header sees which functions are compile-time only and which globals are safe before main, without opening a single body.

An immediate function

A consteval function is an immediate function: every call is evaluated during the build, and a call with a run-time argument is a compile error. There is no run-time version of it in the binary.

The program calls triple(14) and prints 42. The compiler computed the value, and the machine code contains the constant rather than a call.

The keyword is for a function whose result is only meaningful as a constant: a hash of a string literal, a unit conversion for a template, a lookup in a table known at build time. A constexpr function is the general tool, and consteval is the narrow promise.

#include <iostream> consteval int triple(int value) { return value * 3; } int main() { std::cout << triple(14) << '\n'; }

g++ -std=c++23 -Wall -Wextra consteval.cpp -o consteval_demo
./consteval_demo

42

A global that needs no startup code

A global with a dynamic initialiser runs code before main, and the order between translation units is not defined, which is the static initialisation order problem. constinit rejects such an initialiser at compile time.

The program's global start is initialised with a literal, which is constant initialisation, and the print is 100 and 1. The function with the static counter shows the kind of initialisation that constinit would reject.

constinit does not make the variable const: it can be modified at run time. It only says that the value exists before any code runs, which is the guarantee a global needs to be safe to use from another global's constructor.

#include <iostream> int next_id() { static int id = 0; return ++id; } constinit int start = 100; int main() { std::cout << start << ' ' << next_id() << '\n'; }

g++ -std=c++23 -Wall -Wextra constinit.cpp -o constinit_demo
./constinit_demo

100 1

A value the type depends on

An array length has to be a constant expression, so a consteval function fits where a run-time one does not. The program uses square(3) as the bound and prints the number of elements.

The output is 9, the length the compiler computed, and the array is 36 bytes. The call is not in the binary; only the number is.

This is the same rule as the constexpr variable of the previous lesson, with the additional promise that the function can never be called at run time. The promise is what allows the compiler to reject a call with a variable argument at the point of the call.

#include <iostream> consteval int square(int value) { return value * value; } int main() { int values[square(3)]{}; std::cout << sizeof(values) / sizeof(values[0]) << '\n'; }

g++ -std=c++23 -Wall -Wextra array.cpp -o consteval_array
./consteval_array

9

An initialiser that runs code

constinit requires a constant initialiser. A function call that is not constexpr is a run-time initialisation, and the compiler reports that the variable does not have a constant initializer.

The program initialises a global from a plain function and asks for constinit. The error names the variable and the requirement, and the note points at the function that cannot be evaluated by the compiler.

The fix is a constexpr function, or a literal, or removing the constinit and accepting the dynamic initialisation. The last is the choice only when the variable is not used by another global's initialiser, which is the case constinit exists to make safe.

int next() { return 5; } constinit int value = next(); int main() { return value; }

g++ -std=c++23 -Wall -Wextra dynamic.cpp -o dynamic

dynamic.cpp:6:15: error: 'constinit' variable 'value' does not have a constant initializer
6 | constinit int value = next();
| ^~~~~
dynamic.cpp:6:27: error: call to non-'constexpr' function 'int next()'
6 | constinit int value = next();
| ~~~~^~
dynamic.cpp:1:5: note: 'int next()' declared here
1 | int next()
| ^~~~

Exercises

  1. Write a consteval function that returns the length of a string literal and check it with a static_assert.
  2. Change the constinit global to a constexpr one and explain which guarantee is lost.
  3. Call the consteval function with a value read from std::cin and read the compile error.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does consteval require of every call?

    Answer

    It is evaluated during the build, and a run-time argument is a compile error

    An immediate function exists only in the compiler, so every call has to be a constant expression.

  2. What does constinit require?

    Answer

    A constant initialiser for a variable with static storage duration

    It forbids dynamic initialisation, which is the code that runs before main and in an undefined order.

  3. Does constinit make a variable const?

    Answer

    No, the variable can still be modified at run time

    The keyword constrains the initialisation, not the mutability.

  4. What does this program print? #include <iostream> consteval int triple(int value) { return value * 3; } int main() { std::cout << triple(14); }

    Answer

    42

    The immediate function computes 42 during the build, and the program prints the constant.

  5. What does this program print? #include <iostream> int next_id() { static int id = 0; return ++id; } constinit int start = 100; int main() { std::cout << start << ' ' << next_id(); }

    Answer

    100 1

    start is 100 before main, and the first call of next_id returns 1.

  6. What does this program print? #include <iostream> consteval int square(int value) { return value * value; } int main() { int values[square(3)]{}; std::cout << sizeof(values) / sizeof(values[0]); }

    Answer

    9

    square(3) is 9 at compile time, and the program prints the element count.

  7. What happens when a consteval function is called with a run-time value?

    Answer

    The program does not compile

    There is no run-time entry point for an immediate function, so the call cannot be generated.

  8. Which problem does constinit address?

    Answer

    The static initialisation order between translation units

    Constant initialisation happens before any code, so another global can use the value safely.

  9. Can a consteval function be called through a function pointer?

    Answer

    No, it has no run-time address to call

    An immediate function exists at compile time only, so there is nothing for the pointer to refer to.

  10. What does the compiler say about a constinit global initialised from a plain function?

    Answer

    That the variable does not have a constant initializer

    The call would run at startup, and constinit rejects exactly that.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev

Banner image