Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 2 – Types, values and expressions

C++ const, constexpr, consteval and constinit

When a C++ value is fixed and when it is computed: const, constexpr variables and functions, consteval, if consteval, constinit and static_assert.

  • Beginner
  • 25 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Declare values that never change as const
  • Move work to compile time with constexpr and consteval
  • Use constinit to avoid static initialisation order problems

Before you start

The four keywords answer two questions about a value: may it change, and when is it worked out? const makes a variable read-only once it is initialised, even when the value is known only while the program runs. constexpr on a variable means the compiler computes the value, so it can size an array or be checked by static_assert; on a function it means the function may also run while the program is being compiled. consteval (C++20) makes a function that runs only at compile time, and constinit (C++20) makes the compiler initialise a global or static variable that the program may still change later. Use const for every value that should not change, and constexpr whenever the value is known at compile time: work done by the compiler costs nothing at run time, and many mistakes, overflow included, become build errors instead of wrong answers. The examples below are built with Apple clang 21 and GCC 16.

const: read-only after initialisation

A const variable gets its value once, when it is defined, and the compiler refuses every later attempt to change it. The value itself may come from anywhere, including the command line:

read_only.cpp C++
// const: a value that never changes after it is initialised,
// even when it is only known at run time.
#include <print>
#include <string>

int main(int argc, char* argv[]) {
    const int words = argc - 1;                 // known only when the program runs
    const std::string first = argc > 1 ? argv[1] : "(none)";
    constexpr int max_words = 3;                // known when the program is compiled

    std::println("{} words, the first is {}", words, first);
    std::println("{}", words <= max_words ? "short enough" : "too long");
}

The session runs it with two and with four words, and then compiles a file that makes three mistakes:

read_only_errors.cpp C++
// Three mistakes that the compiler rejects.
#include <string>

int main(int argc, char*[]) {
    const int words = argc - 1;
    words = 0;                                  // 1. assigning to a const variable

    const std::string name;                     // fine: std::string has a default value, ""
    const int limit;                            // 2. a const int must be initialised

    constexpr int count = argc;                 // 3. argc is not known at compile time
    return name.size() + limit + count;
}
const and constexpr variables, and three mistakes Python · read_only_session.py
from session import run

run("""
clang++ --version | head -n 1
clang++ -std=c++23 -Wall -Wextra read_only.cpp -o read_only && ./read_only idli dosa
g++-16 --version | head -n 1
g++-16 -std=c++23 -Wall -Wextra read_only.cpp -o read_only && ./read_only idli dosa vada upma
clang++ -std=c++23 -Wall -Wextra read_only_errors.cpp -o read_only_errors
g++-16 -std=c++23 -Wall -Wextra read_only_errors.cpp -o read_only_errors
""")

Output

$ clang++ --version | head -n 1
Apple clang version 21.0.0 (clang-2100.3.34.2)
$ clang++ -std=c++23 -Wall -Wextra read_only.cpp -o read_only && ./read_only idli dosa
2 words, the first is idli
short enough
$ g++-16 --version | head -n 1
g++-16 (Homebrew GCC 16.2.0) 16.2.0
$ g++-16 -std=c++23 -Wall -Wextra read_only.cpp -o read_only && ./read_only idli dosa vada upma
4 words, the first is idli
too long
$ clang++ -std=c++23 -Wall -Wextra read_only_errors.cpp -o read_only_errors
read_only_errors.cpp:6:11: error: cannot assign to variable 'words' with const-qualified type 'const int'
    6 |     words = 0;                                  // 1. assigning to a const variable
      |     ~~~~~ ^
read_only_errors.cpp:5:15: note: variable 'words' declared const here
    5 |     const int words = argc - 1;
      |     ~~~~~~~~~~^~~~~~~~~~~~~~~~
read_only_errors.cpp:9:15: error: default initialization of an object of const type 'const int'
    9 |     const int limit;                            // 2. a const int must be initialised
      |               ^
      |                     = 0
read_only_errors.cpp:11:19: error: constexpr variable 'count' must be initialized by a constant expression
   11 |     constexpr int count = argc;                 // 3. argc is not known at compile time
      |                   ^       ~~~~
read_only_errors.cpp:11:27: note: function parameter 'argc' with unknown value cannot be used in a constant expression
   11 |     constexpr int count = argc;                 // 3. argc is not known at compile time
      |                           ^
read_only_errors.cpp:4:14: note: declared here
    4 | int main(int argc, char*[]) {
      |              ^
3 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra read_only_errors.cpp -o read_only_errors
read_only_errors.cpp: In function 'int main(int, char**)':
read_only_errors.cpp:6:11: error: assignment of read-only variable 'words'
    6 |     words = 0;                                  // 1. assigning to a const variable
      |     ~~~~~~^~~
read_only_errors.cpp:9:15: error: uninitialized 'const limit' [-fpermissive]
    9 |     const int limit;                            // 2. a const int must be initialised
      |               ^~~~~
read_only_errors.cpp:11:27: error: 'argc' is not a constant expression
   11 |     constexpr int count = argc;                 // 3. argc is not known at compile time
      |                           ^~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 read_only_session.py

The three errors show the rules:

  1. Assigning to words fails because const is part of its type, const int.
  2. A const int must be initialised where it is defined, since nothing could set it later. const std::string name; is accepted, because std::string has a default value of its own, the empty string.
  3. max_words is constexpr because 3 is known while compiling, and that makes it usable wherever the language needs a constant: an array size, a template argument, a case label. count cannot be constexpr, because argc exists only when the program runs.

A constexpr variable is also const. Make every variable const unless it must change: the compiler then guarantees that nothing else changes it, and readers know that its value holds from the definition to the end of its scope.

constexpr functions: compile time when it is needed

A constexpr function can be called from a constant expression, and then the compiler runs it. It is an ordinary function as well, which the program may call while it runs. One factorial serves both:

factorials.cpp C++
// One constexpr function, used at compile time (the table, the static_asserts)
// and at run time (the calls in main).
#include <array>
#include <cstdint>
#include <print>

constexpr std::int64_t factorial(int n) {
    std::int64_t result = 1;
    for (int i = 2; i <= n; ++i) result *= i;
    return result;
}

// Built by the compiler: the program contains the 21 finished numbers,
// not the loop that made them.
constexpr auto table = [] {
    std::array<std::int64_t, 21> t{};
    for (int n = 0; n <= 20; ++n) t[n] = factorial(n);
    return t;
}();

static_assert(factorial(0) == 1);
static_assert(factorial(10) == 3'628'800);
static_assert(table[20] == 2'432'902'008'176'640'000);  // 20! is the largest that fits in 64 bits

int main(int argc, char*[]) {
    const int n = argc + 4;  // known only at run time: 5 when the program gets no arguments
    std::println("{}! = {} (computed now), table[{}] = {} (computed by the compiler)", n, factorial(n), n,
                 table[n]);
}
factorials.cpp: compile-time table, run-time calls Python · factorials_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra factorials.cpp -o factorials && ./factorials && ./factorials a b c
g++-16 -std=c++23 -Wall -Wextra factorials.cpp -o factorials && ./factorials && ./factorials a b c
""")

Output

$ clang++ -std=c++23 -Wall -Wextra factorials.cpp -o factorials && ./factorials && ./factorials a b c
5! = 120 (computed now), table[5] = 120 (computed by the compiler)
8! = 40320 (computed now), table[8] = 40320 (computed by the compiler)
$ g++-16 -std=c++23 -Wall -Wextra factorials.cpp -o factorials && ./factorials && ./factorials a b c
5! = 120 (computed now), table[5] = 120 (computed by the compiler)
8! = 40320 (computed now), table[8] = 40320 (computed by the compiler)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 factorials_session.py

The 21 entries of table were computed by the compiler (the lambda that builds them is called immediately, and lambdas can run at compile time since C++17), and the three static_assert lines were checked before the program existed. The calls in main use a number known only at run time, so they run as ordinary code. The compiler must evaluate a constexpr function when its result is needed in a constant expression, as for table and in static_assert; elsewhere it may run the call at either time.

Undefined behaviour becomes a compile error

21! is 51,090,942,171,709,440,000, too large for a 64-bit signed integer. Asked at run time, the multiplication overflows, which is undefined behaviour; asked at compile time, the same code is rejected, because a constant expression may never contain undefined behaviour:

21! at run time and at compile time

overflow.cpp

// 21! does not fit in 64 bits. Asked at run time,
// the multiplication overflows: undefined behaviour.
#include <cstdint>
#include <print>

constexpr std::int64_t factorial(int n) {
    std::int64_t result = 1;
    for (int i = 2; i <= n; ++i) result *= i;
    return result;
}

int main(int argc, char*[]) {
    const int n = argc + 20;  // 21, known only at run time
    std::println("{}! = {}", n, factorial(n));
}

overflow_constant.cpp

// The same overflow while the compiler evaluates factorial(21): the build stops.
#include <cstdint>

constexpr std::int64_t factorial(int n) {
    std::int64_t result = 1;
    for (int i = 2; i <= n; ++i) result *= i;
    return result;
}

constexpr std::int64_t too_big = factorial(21);

int main() {}
The overflow, reported by UBSan, then refused by the compiler Python · overflow_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=undefined overflow.cpp -o overflow && ./overflow
g++-16 -std=c++23 -Wall -Wextra -fsanitize=undefined overflow.cpp -o overflow && ./overflow
clang++ -std=c++23 -Wall -Wextra overflow_constant.cpp -o overflow_constant
g++-16 -std=c++23 -Wall -Wextra overflow_constant.cpp -o overflow_constant
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -fsanitize=undefined overflow.cpp -o overflow && ./overflow
overflow.cpp:8:41: runtime error: signed integer overflow: 2432902008176640000 * 21 cannot be represented in type 'std::int64_t' (aka 'long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior overflow.cpp:8:41
21! = -4249290049419214848
$ g++-16 -std=c++23 -Wall -Wextra -fsanitize=undefined overflow.cpp -o overflow && ./overflow
overflow.cpp:8:41: runtime error: signed integer overflow: 2432902008176640000 * 21 cannot be represented in type 'long long int'
21! = -4249290049419214848
$ clang++ -std=c++23 -Wall -Wextra overflow_constant.cpp -o overflow_constant
overflow_constant.cpp:10:24: error: constexpr variable 'too_big' must be initialized by a constant expression
   10 | constexpr std::int64_t too_big = factorial(21);
      |                        ^         ~~~~~~~~~~~~~
overflow_constant.cpp:6:41: note: value 51090942171709440000 is outside the range of representable values of type 'std::int64_t' (aka 'long long')
    6 |     for (int i = 2; i <= n; ++i) result *= i;
      |                                         ^
overflow_constant.cpp:10:34: note: in call to 'factorial(21)'
   10 | constexpr std::int64_t too_big = factorial(21);
      |                                  ^~~~~~~~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra overflow_constant.cpp -o overflow_constant
overflow_constant.cpp:10:43:   in 'constexpr' expansion of 'factorial(21)'
   10 | constexpr std::int64_t too_big = factorial(21);
      |                                  ~~~~~~~~~^~~~
overflow_constant.cpp:6:41: error: overflow in constant expression [-fpermissive]
    6 |     for (int i = 2; i <= n; ++i) result *= i;
      |                                  ~~~~~~~^~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 overflow_session.py

At run time UndefinedBehaviorSanitizer reported the overflow, and the program printed a negative “factorial”. At compile time both compilers stopped the build, Clang with the exact value that does not fit. This is one of the best reasons to compute what you can with constexpr: tests written as static_assert cannot hide undefined behaviour.

Compile-time work has a budget

The compiler gives up on very long constant evaluations, so that a loop that never ends cannot hang the build. Clang’s -fconstexpr-steps allows 1,048,576 steps by default; GCC’s -fconstexpr-loop-limit allows 262,144 iterations of any one loop, and -fconstexpr-ops-limit limits the total work:

compile_time_budget.cpp C++
// Work done by the compiler has a budget:
// a loop of two million steps is over it on both compilers.
constexpr long long sum_to(int n) {
    long long sum = 0;
    for (int i = 1; i <= n; ++i) sum += i;
    return sum;
}

static_assert(sum_to(100'000) == 5'000'050'000);          // fine
static_assert(sum_to(2'000'000) == 2'000'001'000'000);    // too many steps

int main() {}
Two million iterations at compile time Python · budget_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra compile_time_budget.cpp -o compile_time_budget
g++-16 -std=c++23 -Wall -Wextra compile_time_budget.cpp -o compile_time_budget
""")

Output

$ clang++ -std=c++23 -Wall -Wextra compile_time_budget.cpp -o compile_time_budget
compile_time_budget.cpp:10:15: error: static assertion expression is not an integral constant expression
   10 | static_assert(sum_to(2'000'000) == 2'000'001'000'000);    // too many steps
      |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compile_time_budget.cpp:5:34: note: constexpr evaluation hit maximum step limit; possible infinite loop?
    5 |     for (int i = 1; i <= n; ++i) sum += i;
      |                                  ^
compile_time_budget.cpp:10:15: note: in call to 'sum_to(2000000)'
   10 | static_assert(sum_to(2'000'000) == 2'000'001'000'000);    // too many steps
      |               ^~~~~~~~~~~~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra compile_time_budget.cpp -o compile_time_budget
compile_time_budget.cpp:10:33: error: non-constant condition for static assertion
   10 | static_assert(sum_to(2'000'000) == 2'000'001'000'000);    // too many steps
      |               ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~
compile_time_budget.cpp:10:21:   in 'constexpr' expansion of 'sum_to(2000000)'
   10 | static_assert(sum_to(2'000'000) == 2'000'001'000'000);    // too many steps
      |               ~~~~~~^~~~~~~~~~~
compile_time_budget.cpp:5:5: error: 'constexpr' loop iteration count exceeds limit of 262144 (use '-fconstexpr-loop-limit=' to increase the limit)
    5 |     for (int i = 1; i <= n; ++i) sum += i;
      |     ^~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 budget_session.py

Both refuse the second static_assert. The limits can be raised with those options, but a build that needs millions of steps of constant evaluation is usually better served by a table generated once, or by computing at run time.

consteval: only at compile time

A consteval function, an immediate function, must be computed by the compiler at every call. Use it when a result must never cost anything at run time, or when a check must happen in the build:

A consteval hash, called correctly and with run-time text

name_hash.cpp

// consteval: every call must be computed by the compiler.
// Here: a 32-bit hash of a name (FNV-1a).
#include <cstdint>
#include <print>
#include <string_view>

consteval std::uint32_t name_hash(std::string_view name) {
    std::uint32_t h = 2'166'136'261u;
    for (char c : name) {
        h ^= static_cast<unsigned char>(c);
        h *= 16'777'619u;
    }
    return h;
}

int main() {
    constexpr std::uint32_t delhi = name_hash("delhi");  // the program contains only the result
    std::println("name_hash(\"delhi\") = {:#010x}", delhi);
    // also computed while compiling
    std::println("name_hash(\"pune\")  = {:#010x}", name_hash("pune"));
}

name_hash_runtime.cpp

// The same consteval function, called with text that exists only at run time: rejected.
#include <cstdint>
#include <print>
#include <string_view>

consteval std::uint32_t name_hash(std::string_view name) {
    std::uint32_t h = 2'166'136'261u;
    for (char c : name) {
        h ^= static_cast<unsigned char>(c);
        h *= 16'777'619u;
    }
    return h;
}

int main(int argc, char* argv[]) {
    if (argc > 1) std::println("{:#010x}", name_hash(argv[1]));
}
consteval: fine with a literal, rejected with argv Python · name_hash_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra name_hash.cpp -o name_hash && ./name_hash
g++-16 -std=c++23 -Wall -Wextra name_hash.cpp -o name_hash && ./name_hash
clang++ -std=c++23 -Wall -Wextra name_hash_runtime.cpp -o name_hash_runtime
g++-16 -std=c++23 -Wall -Wextra name_hash_runtime.cpp -o name_hash_runtime
""")

Output

$ clang++ -std=c++23 -Wall -Wextra name_hash.cpp -o name_hash && ./name_hash
name_hash("delhi") = 0x4abc253d
name_hash("pune")  = 0x762881ff
$ g++-16 -std=c++23 -Wall -Wextra name_hash.cpp -o name_hash && ./name_hash
name_hash("delhi") = 0x4abc253d
name_hash("pune")  = 0x762881ff
$ clang++ -std=c++23 -Wall -Wextra name_hash_runtime.cpp -o name_hash_runtime
name_hash_runtime.cpp:16:44: error: call to consteval function 'name_hash' is not a constant expression
   16 |     if (argc > 1) std::println("{:#010x}", name_hash(argv[1]));
      |                                            ^
name_hash_runtime.cpp:16:54: note: function parameter 'argv' with unknown value cannot be used in a constant expression
   16 |     if (argc > 1) std::println("{:#010x}", name_hash(argv[1]));
      |                                                      ^
name_hash_runtime.cpp:15:26: note: declared here
   15 | int main(int argc, char* argv[]) {
      |                          ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra name_hash_runtime.cpp -o name_hash_runtime
name_hash_runtime.cpp: In function 'int main(int, char**)':
name_hash_runtime.cpp:16:53: error: call to consteval function 'name_hash(std::basic_string_view<char>(((const char*)(*(argv + 8)))))' is not a constant expression
   16 |     if (argc > 1) std::println("{:#010x}", name_hash(argv[1]));
      |                                            ~~~~~~~~~^~~~~~~~~
name_hash_runtime.cpp:16:54: error: 'argv' is not a constant expression
   16 |     if (argc > 1) std::println("{:#010x}", name_hash(argv[1]));
      |                                                      ^~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 name_hash_session.py

Both hashes were computed while compiling, so the program contains only the two numbers. The second file passes text from the command line, which the compiler cannot know, and both compilers reject the call. With constexpr instead of consteval, the same call would have compiled and run at run time.

The standard library uses consteval for a check you meet every day. The format string of std::println and std::format is a std::basic_format_string, whose constructor is consteval (P2216R3), so a format that does not fit its arguments is a build error:

format_check.cpp C++
// std::println checks its format string while compiling, with a consteval constructor:
// {:d} asks for a number, but the argument is text.
#include <print>

int main() {
    std::println("{} has {:d} letters", "Kochi", "five");
}
A format string checked while compiling Python · format_check_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra format_check.cpp -o format_check
g++-16 -std=c++23 -Wall -Wextra format_check.cpp -o format_check
""", max_lines=4)

Output

$ clang++ -std=c++23 -Wall -Wextra format_check.cpp -o format_check
format_check.cpp:6:18: error: call to consteval function 'std::basic_format_string<char, const char (&)[6], const char (&)[5]>::basic_format_string<char[20]>' is not a constant expression
    6 |     std::println("{} has {:d} letters", "Kochi", "five");
      |                  ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__format/parser_std_format_spec.h:889:5: note: non-constexpr function '__throw_format_error' cannot be used in a constant expression
[29 more lines]
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra format_check.cpp -o format_check
format_check.cpp: In function 'int main()':
format_check.cpp:6:18: error: call to consteval function 'std::basic_format_string<char, const char (&)[6], const char (&)[5]>("{} has {:d} letters")' is not a constant expression
    6 |     std::println("{} has {:d} letters", "Kochi", "five");
      |                  ^~~~~~~~~~~~~~~~~~~~~
[30 more lines]
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 format_check_session.py

if consteval: one function, two paths

C++23 adds if consteval, which takes its first branch when the function is being evaluated by the compiler and its else branch at run time. In this example the run-time branch may print, which a constant expression cannot, and treats a bad digit differently:

two_paths.cpp C++
// if consteval (C++23): one function, one path when the compiler evaluates it,
// another at run time.
#include <print>

constexpr int digit_value(char c) {
    if consteval {
        // Compile time: a bad digit stops the build,
        // because a throw that escapes is not a constant expression.
        if (c < '0' || c > '9') throw "not a digit";
    } else {
        // Run time: printing is allowed here, and a bad digit becomes -1.
        std::println("digit_value('{}') ran while the program was running", c);
        if (c < '0' || c > '9') return -1;
    }
    return c - '0';
}

constexpr int seven = digit_value('7');  // evaluated by the compiler: prints nothing
static_assert(seven == 7);

int main(int argc, char*[]) {
    const char c = argc > 1 ? 'x' : '4';
    std::println("seven = {}, digit_value('{}') = {}", seven, c, digit_value(c));
}
if consteval: the compile-time call prints nothing Python · two_paths_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra two_paths.cpp -o two_paths && ./two_paths && ./two_paths x
g++-16 -std=c++23 -Wall -Wextra two_paths.cpp -o two_paths && ./two_paths && ./two_paths x
""")

Output

$ clang++ -std=c++23 -Wall -Wextra two_paths.cpp -o two_paths && ./two_paths && ./two_paths x
digit_value('4') ran while the program was running
seven = 7, digit_value('4') = 4
digit_value('x') ran while the program was running
seven = 7, digit_value('x') = -1
$ g++-16 -std=c++23 -Wall -Wextra two_paths.cpp -o two_paths && ./two_paths && ./two_paths x
digit_value('4') ran while the program was running
seven = 7, digit_value('4') = 4
digit_value('x') ran while the program was running
seven = 7, digit_value('x') = -1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 two_paths_session.py

seven was computed by the compiler, so its call printed nothing; each call in main printed its line. C++20 had std::is_constant_evaluated() for the same purpose, but it is easy to misuse: inside if constexpr it is always true, a trap that P1938R3 gives as one reason for if consteval.

constinit: set by the compiler, not constant

Global and static variables whose initial value needs code to run are initialised by that code when the program starts, and between source files the standard leaves the order open, so one global can read another before it has its value: the static initialisation order problem. A variable declared constinit must get its initial value from a constant expression, during static initialisation, so it is ready before any code runs. Unlike constexpr, it does not make the variable const:

A constinit counter, and a constinit variable that needs run-time code

constinit_counter.cpp

// constinit (C++20): a global that the compiler must initialise,
// and that the program may still change.
#include <print>

constexpr int start_value() { return 100; }

// fixed at compile time: no code runs before main to set it
constinit int next_ticket = start_value();

int take_ticket() { return next_ticket++; }  // not const: it changes while the program runs

int main() {
    const int a = take_ticket();
    const int b = take_ticket();
    std::println("tickets {} and {}, the next one is {}", a, b, next_ticket);
}

constinit_error.cpp

// A constinit variable whose initial value can only be computed at run time: rejected.
#include <cstdlib>

int start_value() { return std::atoi("100"); }  // not constexpr

constinit int next_ticket = start_value();

int main() { return next_ticket; }
constinit: accepted with a constexpr initialiser, refused otherwise Python · constinit_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra constinit_counter.cpp -o constinit_counter && ./constinit_counter
g++-16 -std=c++23 -Wall -Wextra constinit_counter.cpp -o constinit_counter && ./constinit_counter
clang++ -std=c++23 -Wall -Wextra constinit_error.cpp -o constinit_error
g++-16 -std=c++23 -Wall -Wextra constinit_error.cpp -o constinit_error
""")

Output

$ clang++ -std=c++23 -Wall -Wextra constinit_counter.cpp -o constinit_counter && ./constinit_counter
tickets 100 and 101, the next one is 102
$ g++-16 -std=c++23 -Wall -Wextra constinit_counter.cpp -o constinit_counter && ./constinit_counter
tickets 100 and 101, the next one is 102
$ clang++ -std=c++23 -Wall -Wextra constinit_error.cpp -o constinit_error
constinit_error.cpp:6:15: error: variable does not have a constant initializer
    6 | constinit int next_ticket = start_value();
      |               ^             ~~~~~~~~~~~~~
constinit_error.cpp:6:1: note: required by 'constinit' specifier here
    6 | constinit int next_ticket = start_value();
      | ^~~~~~~~~
constinit_error.cpp:6:29: note: non-constexpr function 'start_value' cannot be used in a constant expression
    6 | constinit int next_ticket = start_value();
      |                             ^
constinit_error.cpp:4:5: note: declared here
    4 | int start_value() { return std::atoi("100"); }  // not constexpr
      |     ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra constinit_error.cpp -o constinit_error
constinit_error.cpp:6:15: error: 'constinit' variable 'next_ticket' does not have a constant initializer
    6 | constinit int next_ticket = start_value();
      |               ^~~~~~~~~~~
constinit_error.cpp:6:40: error: call to non-'constexpr' function 'int start_value()'
    6 | constinit int next_ticket = start_value();
      |                             ~~~~~~~~~~~^~
constinit_error.cpp:4:5: note: 'int start_value()' declared here
    4 | int start_value() { return std::atoi("100"); }  // not constexpr
      |     ^~~~~~~~~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 constinit_session.py

next_ticket starts at 100, set by the compiler, and the program then changes it. In the second file start_value() is an ordinary function, so the initialisation would need code at run time, and constinit turns that into an error rather than a hidden ordering risk. constinit applies only to variables with static or thread storage duration (globals, static variables and thread_local ones), and it does not make access from several threads safe.

static_assert, now with computed messages

static_assert(condition, message) stops the build when the condition is false; since C++17 the message is optional. C++26 accepts a message computed by the compiler (P2741R3), any constant expression with size() and data(), so the error can say what was found:

assert_message.cpp C++
// static_assert with a message that the compiler computes (C++26, paper P2741):
// the message says what it found.
#include <cstddef>
#include <string>

// std::to_string cannot run at compile time, so this small function writes the digits instead.
constexpr std::string digits(std::size_t n) {
    std::string s;
    do {
        s.insert(s.begin(), static_cast<char>('0' + n % 10));
        n /= 10;
    } while (n != 0);
    return s;
}

template <typename T>
constexpr std::string size_message() {
    return "expected an 8-byte type, but this one has " + digits(sizeof(T)) + " bytes";
}

static_assert(sizeof(long long) == 8, size_message<long long>());
static_assert(sizeof(int) == 8, size_message<int>());

int main() {}
A computed static_assert message Python · assert_message_session.py
from session import run

run("""
clang++ -std=c++2c -Wall -Wextra assert_message.cpp -o assert_message
g++-16 -std=c++26 -Wall -Wextra assert_message.cpp -o assert_message
# The same file as C++23: Clang accepts the computed message as an extension, with a warning.
clang++ -std=c++23 -Wall -Wextra assert_message.cpp -o assert_message
""")

Output

$ clang++ -std=c++2c -Wall -Wextra assert_message.cpp -o assert_message
assert_message.cpp:22:15: error: static assertion failed due to requirement 'sizeof(int) == 8': expected an 8-byte type, but this one has 4 bytes
   22 | static_assert(sizeof(int) == 8, size_message<int>());
      |               ^~~~~~~~~~~~~~~~
assert_message.cpp:22:27: note: expression evaluates to '4 == 8'
   22 | static_assert(sizeof(int) == 8, size_message<int>());
      |               ~~~~~~~~~~~~^~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++26 -Wall -Wextra assert_message.cpp -o assert_message
assert_message.cpp:22:27: error: static assertion failed: expected an 8-byte type, but this one has 4 bytes
   22 | static_assert(sizeof(int) == 8, size_message<int>());
      |               ~~~~~~~~~~~~^~~~
  • the comparison reduces to '(4 == 8)'
[exit status 1]
# The same file as C++23: Clang accepts the computed message as an extension, with a warning.
$ clang++ -std=c++23 -Wall -Wextra assert_message.cpp -o assert_message
assert_message.cpp:21:39: warning: 'static_assert' with a user-generated message is a C++26 extension [-Wc++26-extensions]
   21 | static_assert(sizeof(long long) == 8, size_message<long long>());
      |                                       ^
assert_message.cpp:22:33: warning: 'static_assert' with a user-generated message is a C++26 extension [-Wc++26-extensions]
   22 | static_assert(sizeof(int) == 8, size_message<int>());
      |                                 ^
assert_message.cpp:22:15: error: static assertion failed due to requirement 'sizeof(int) == 8': expected an 8-byte type, but this one has 4 bytes
   22 | static_assert(sizeof(int) == 8, size_message<int>());
      |               ^~~~~~~~~~~~~~~~
assert_message.cpp:22:27: note: expression evaluates to '4 == 8'
   22 | static_assert(sizeof(int) == 8, size_message<int>());
      |               ~~~~~~~~~~~~^~~~
2 warnings and 1 error generated.
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 assert_message_session.py

Clang 21 with -std=c++2c and GCC 16 with -std=c++26 print the computed message, “this one has 4 bytes”. Built as C++23, Clang still understands the computed message, with a warning that it is a C++26 extension; the assertion fails in every run, as it should on a platform where int has 4 bytes. The helper digits exists because std::to_string cannot run at compile time.

The four keywords compared

const or constexpr?
Criterion constconstexpr
Applies to variables (and member functions, in a later module)variables and functions
When is the value known? whenever the initialiser runsat compile time for variables; at either time for function calls
Can it change later? nono (a constexpr variable is const)
If it cannot be done at compile time not requireda variable is an error; a function call runs at run time
When to choose Every value that must not changeValues and functions the compiler can work out
consteval or constinit?
Criterion constevalconstinit
Applies to functionsvariables with static or thread storage duration
When is the value known? at compile time, at every callat compile time
Can it change later? not a variableyes
If it cannot be done at compile time an erroran error
When to choose Results that must cost nothing at run time, and build-time checksGlobals that must be ready before main and may change

Exercise: Fibonacci numbers at compile time

Write fib_mod(n), which returns the n-th Fibonacci number modulo 1,000,000,007 (with fib_mod(0) == 0 and fib_mod(1) == 1), as a constexpr function with a loop. The ten static_assert lines are the tests, checked while the file compiles: the exercise is solved when it builds without an error.

fibonacci_exercise.cpp C++
// Exercise: fib_mod(n) returns the n-th Fibonacci number modulo 1,000,000,007,
// where fib_mod(0) == 0 and fib_mod(1) == 1. The checks run while the file compiles:
// the exercise is solved when this file builds without an error.
#include <cstdint>

constexpr std::int64_t fib_mod(int n) {
    (void)n;
    return 0;  // replace this with your loop
}

static_assert(fib_mod(0) == 0);
static_assert(fib_mod(1) == 1);
static_assert(fib_mod(2) == 1);
static_assert(fib_mod(10) == 55);
static_assert(fib_mod(46) == 836'311'896);
static_assert(fib_mod(50) == 586'268'941);   // 12,586,269,025 before the modulo
static_assert(fib_mod(90) == 210'345'902);
static_assert(fib_mod(100) == 687'995'182);
static_assert(fib_mod(1'000) == 517'691'607);
static_assert(fib_mod(100'000) == 911'435'502);

int main() {}

As given, the file fails nine of the ten checks (only fib_mod(0) == 0 holds):

The exercise as given Python · fibonacci_exercise_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra fibonacci_exercise.cpp -o fibonacci_exercise
""", max_lines=10)

Output

$ clang++ -std=c++23 -Wall -Wextra fibonacci_exercise.cpp -o fibonacci_exercise
fibonacci_exercise.cpp:12:15: error: static assertion failed due to requirement 'fib_mod(1) == 1'
   12 | static_assert(fib_mod(1) == 1);
      |               ^~~~~~~~~~~~~~~
fibonacci_exercise.cpp:12:26: note: expression evaluates to '0 == 1'
   12 | static_assert(fib_mod(1) == 1);
      |               ~~~~~~~~~~~^~~~
fibonacci_exercise.cpp:13:15: error: static assertion failed due to requirement 'fib_mod(2) == 1'
   13 | static_assert(fib_mod(2) == 1);
      |               ^~~~~~~~~~~~~~~
fibonacci_exercise.cpp:13:26: note: expression evaluates to '0 == 1'
[45 more lines]
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fibonacci_exercise_session.py

First hint: keep two variables for the last two numbers, and take the remainder after every addition, so the values never exceed 2,000,000,012 and fit easily in std::int64_t. Then try fib_mod(1'000'000) in a static_assert: the budget section above explains what happens.

Self-check

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 read_number() is an ordinary function declared as int read_number();. Which of the four lines does not compile?

    Read the code, then choose one answer.

    const int a = read_number();
    constexpr int b = 6 * 7;
    constexpr int c = read_number();
    const int d = 6 * 7;
    Show the answer to question 1

    Answer: The line that defines c

    A constexpr variable needs a value the compiler can compute, and an ordinary function call cannot be computed while compiling. A const variable may take its value at run time, so a is fine; b and d are both initialised with a constant.

  2. Question 2 of 5 square is declared consteval int square(int n) { return n * n; }. Which line inside main is an error?

    Read the code, then choose one answer.

    int x = 5;
    constexpr int a = square(5);
    int b = square(5);
    int c = square(x);
    Show the answer to question 2

    Answer: int c = square(x);

    Every call of a consteval function must be a constant expression. square(5) is one wherever it appears, so b is fine even though b is an ordinary variable. x is not const, so its value cannot be used by the compiler, and square(x) is rejected by both compilers.

  3. Question 3 of 5 constexpr int f(int n) { return n * n; } is called as return f(argc); in main. What happens?

    Choose one answer.

    Show the answer to question 3

    Answer: It compiles, and f runs while the program runs

    A constexpr function may run at compile time but does not have to. argc is known only at run time, and return f(argc); does not need a constant, so the call is ordinary code. With consteval it would be an error.

  4. Question 4 of 5 Which statements about constinit are true?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • The initial value must come from a constant expression
    • It applies only to variables with static or thread storage duration
    • The variable may be changed later

    constinit guarantees that the compiler initialises the variable, and nothing more: the variable is not const. Both compilers reject it on an ordinary local variable, because it applies only to globals, static variables and thread_local ones.

  5. Question 5 of 5 By default, how many iterations of a single loop does GCC allow while it evaluates a constant expression?

    Type a number.

    Show the answer to question 5

    Answer: 262144

    GCC's -fconstexpr-loop-limit is 262,144 (2 to the power 18) by default, which is why sum_to(2'000'000) failed in the budget example. Clang counts evaluation steps instead, 1,048,576 by default.

Interview questions

What is the difference between const and constexpr? const promises that a variable will not change after it is initialised, but its value may be computed at run time, from input for example. constexpr on a variable demands that the compiler can compute the value, so the variable is usable in constant expressions such as array sizes, template arguments and static_assert; a constexpr variable is also const. On a function, constexpr allows calls at compile time without requiring them: a call runs at compile time when its result is needed in a constant expression, and as ordinary code otherwise.

Key takeaways

  • const means read-only after initialisation; the value may come from run time.
  • A constexpr variable is computed by the compiler and is const; a constexpr function may run at compile time or at run time, and must run at compile time when a constant expression needs its result.
  • Undefined behaviour is never allowed in a constant expression, so overflow and out-of-range shifts in constexpr code are build errors, a cheap and strong test.
  • Constant evaluation has limits: Clang allows 1,048,576 steps and GCC 262,144 iterations of a loop by default.
  • consteval functions run only at compile time; std::format and std::println use one to check format strings.
  • if consteval (C++23) gives a function a compile-time branch and a run-time branch.
  • constinit makes the compiler initialise a global or static variable without making it const, which removes the static initialisation order problem for that variable.
  • C++26 lets static_assert print a message computed by the compiler; Clang 21 and GCC 16 both support it.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.