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.
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
What you’ll learn · Before you start
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
On this page
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:
// 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:
// 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;
} 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:
- Assigning to
wordsfails becauseconstis part of its type,const int. - A
const intmust be initialised where it is defined, since nothing could set it later.const std::string name;is accepted, becausestd::stringhas a default value of its own, the empty string. max_wordsisconstexprbecause 3 is known while compiling, and that makes it usable wherever the language needs a constant: an array size, a template argument, acaselabel.countcannot beconstexpr, becauseargcexists 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:
// 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]);
} 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:
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() {} 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:
// 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() {} 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:
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]));
} 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:
// 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");
} 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:
// 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));
} 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:
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; } 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:
// 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() {} 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
| Criterion | const | constexpr |
|---|---|---|
| Applies to | variables (and member functions, in a later module) | variables and functions |
| When is the value known? | whenever the initialiser runs | at compile time for variables; at either time for function calls |
| Can it change later? | no | no (a constexpr variable is const) |
| If it cannot be done at compile time | not required | a variable is an error; a function call runs at run time |
| When to choose | Every value that must not change | Values and functions the compiler can work out |
| Criterion | consteval | constinit |
|---|---|---|
| Applies to | functions | variables with static or thread storage duration |
| When is the value known? | at compile time, at every call | at compile time |
| Can it change later? | not a variable | yes |
| If it cannot be done at compile time | an error | an error |
| When to choose | Results that must cost nothing at run time, and build-time checks | Globals 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.
// 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):
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.
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
constmeans read-only after initialisation; the value may come from run time.- A
constexprvariable is computed by the compiler and isconst; aconstexprfunction 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
constexprcode 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.
constevalfunctions run only at compile time;std::formatandstd::printlnuse one to check format strings.if consteval(C++23) gives a function a compile-time branch and a run-time branch.constinitmakes the compiler initialise a global or static variable without making itconst, which removes the static initialisation order problem for that variable.- C++26 lets
static_assertprint a message computed by the compiler; Clang 21 and GCC 16 both support it.
References
- Working Draft, Programming Languages: C++ (N4950), 7.7 Constant expressions, 9.2.6 The constexpr and consteval specifiers, 9.2.7 The constinit specifier (ISO/IEC JTC1/SC22/WG21)
- P1938R3: if consteval (ISO/IEC JTC1/SC22/WG21)
- P1143R2: Adding the constinit keyword (ISO/IEC JTC1/SC22/WG21)
- P2741R3: user-generated static_assert messages (ISO/IEC JTC1/SC22/WG21)
- P2216R3: std::format improvements (ISO/IEC JTC1/SC22/WG21)
- Clang Compiler User's Manual: -fconstexpr-steps (LLVM Project)
- Options Controlling C++ Dialect: -fconstexpr-loop-limit and -fconstexpr-ops-limit (GNU Project (GCC))
- C++ Core Guidelines: Con.1 By default, make objects immutable; Con.5 Use constexpr for values that can be computed at compile time (Standard C++ Foundation)
- C++ Support in Clang (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress