const, constexpr and consteval all look like different ways to say "constant", but they make different promises. One blocks mutation through an object, one makes a value or function available to constant-expression contexts, and one rejects ordinary runtime calls. A C++20 program demonstrates the distinction: it outputs exactly 6 8 14 81, and two deliberately invalid lines reveal the boundaries. The broader Coding & Skill Development category connects this topic to related programming courses and tutorials.
Three keywords, three different contracts
Think in contracts before thinking in syntax. const asks, "May this object be modified through this name?" A constexpr object must have a constant-expression initializer. A constexpr function can be evaluated at compile time when its arguments and the surrounding context qualify, but it can also serve a runtime caller. A consteval function is immediate, so each ordinary call must succeed as a constant expression. A constexpr object is also const, but an arbitrary const object need not be a constant expression.
In our program, runtime_limit becomes 6 without a compile-time guarantee. double_it(4) supplies the compile-time array size 8, while double_it(user_value) produces 14 in an allowed runtime call when user_value is 7. square(9) produces 81, but square(user_value) is rejected. WG21's P1073R3: Immediate functions defines the immediate-function model and explains why constexpr does not force every call to be evaluated during translation. If these declarations are new, the C++ Tutorial gives a wider learning path.

const: immutable after initialisation does not mean compile-time
Start with an ordinary function and a const object:
int load_limit() { return 6; }
const int runtime_limit = load_limit();The observed value is 6, and runtime_limit = 9; is rejected because the object cannot be assigned after initialisation. However, static_assert(runtime_limit == 6); is ill-formed. The initializer calls a non-constexpr function, so runtime_limit is not usable as a constant expression. The missing property is constant-expression eligibility, not immutability.
Do not turn that into the rule "const is always runtime". An integral declaration such as const int literal_limit = 6; has a constant-expression initializer and can be usable in a constant-expression context. The accurate rule is that const alone does not guarantee compile-time evaluation.
Pointer declarations show why you must identify what is const. With int a = 5, b = 8;, const int* p = &a; permits p = &b; but rejects *p = 9;: the pointed-to integer is protected through p. By contrast, int* const q = &a; permits *q = 9; but rejects q = &b;: the pointer itself cannot be redirected.
constexpr: the call is compile-time only when the context requires it
Consider one function:
constexpr int double_it(int x) { return 2 * x; }Now use it in three contexts that require constant expressions. constexpr int table_size = double_it(4); makes table_size exactly 8. static_assert(double_it(5) == 10); must be checked during translation. Finally, std::array<int, double_it(4)> table{}; has exactly 8 elements because its non-type template argument must be a constant expression.
The same function remains valid here:
int user_value = 7;
int run_result = double_it(user_value);This call produces 14 at runtime. It is not a constant expression because it reads a non-const runtime object, but a constexpr function is allowed to serve that caller. A compiler may still optimise the multiplication; such folding is not a language guarantee supplied by constexpr.
The distinction is between object and function contracts. A constexpr variable is const and must be initialised by a constant expression. A constexpr function can support both compile-time and runtime calls. This section's code is valid in C++17. For adjacent class and object concepts, see Object Oriented Technology Explained, not as evidence for constant-expression rules but as the next conceptual layer in C++.
consteval: C++20 turns failed constant evaluation into a compile error
C++20 adds an immediate function declaration:
consteval int square(int x) { return x * x; }constexpr int token = square(9); is valid and fixes token at 81. Even int also_ok = square(10); is valid. The destination is not constexpr, but the call itself has the constant argument 10, so square(10) must be evaluated as a constant expression and yields 100.
The boundary appears with int user_value = 7; int rejected = square(user_value);. This is ill-formed because an immediate call cannot read that non-constant object. An optimiser noticing that it currently contains 7 does not change the language rule.
The version boundary is equally clear. consteval is absent from C++17 and part of C++20. Compile the full example as C++20. For a C++17 teaching variant, remove square, token, and the fourth printed value; the const and constexpr demonstrations remain.
Worked example: compile one file, then expose both rejected lines
Use this exact positive C++20 program:
#include <array>
#include <iostream>
int load_limit() { return 6; }
constexpr int double_it(int x) { return 2 * x; }
consteval int square(int x) { return x * x; }
int main() {
const int runtime_limit = load_limit();
constexpr int table_size = double_it(4);
std::array<int, table_size> table{};
int user_value = 7;
int run_result = double_it(user_value);
constexpr int token = square(9);
static_assert(double_it(5) == 10);
std::cout << runtime_limit << ' '
<< table.size() << ' '
<< run_result << ' '
<< token << '\n';
}The output is:
6 8 14 81Trace each number instead of memorising the line. load_limit() returns 6, which initialises an immutable object through an ordinary function. double_it(4) computes 2 * 4 = 8; that result becomes both table_size and the std::array size. The runtime call computes double_it(7) = 2 * 7 = 14. The immediate call computes square(9) = 9 * 9 = 81. The array declaration supplies a concrete check: without a constant-expression table_size, its template argument would be rejected. The positive file compiles cleanly as C++20. That confirms the language uses without making any claim about optimiser behaviour.
Next add this line inside main, compile, then remove it:
int rejected = square(user_value);It must fail because user_value is not a constant expression. Then test the second line separately:
static_assert(runtime_limit == 6);It must fail because the call to load_limit() was not a constant expression. Testing one rejected line at a time keeps each diagnostic tied to one rule. The C++17 variant omits the consteval declaration and use, compiles the remaining program as C++17, and prints 6 8 14.

Common traps and how checks expose them
Trap | Correction from the program |
|---|---|
Every const value is compile-time |
|
Every |
|
Optimiser folding proves a language guarantee | Optimisation does not turn a non-constant expression into a guaranteed constant expression. |
| The |
constinit concerns static initialisation. It is neither general immutability nor a rule requiring every later call to be constant-evaluated.
Use these checks to test your explanation:
Why does
std::array<int, table_size>compile with size8?Why may
double_it(user_value)return14?Why does
square(user_value)fail whenuser_valuecurrently stores7?Why is
static_assert(runtime_limit == 6)invalid although the program later prints6?
Each answer should name the constant-expression context and the relevant declaration contract, not claim that "the compiler is smart enough". Also test the version: the unmodified file is valid under C++20, while C++17 rejects the consteval keyword.
The short version and the next step
Use
constwhen mutation must be blocked, as withruntime_limit = 6.Use
constexprwhen a value or reusable function should be available to constant-expression contexts. That coverstable_size = 8while still allowingrun_result = 14.Use
constevalin C++20 when an ordinary direct call must be rejected unless it can be evaluated as a constant expression, as withtoken = 81.
Now change user_value from 7 to 11. Predict double_it(11) = 22, then explain why square(user_value) remains invalid. Continue with the C++ Programming Course for the direct structured route, or use Coding for Placements for a broader multi-language route.




