const, constexpr, consteval and constinit

C++
Author

dev::author

Published

December 29, 2025

const-ness

Constants, by their simplest definition are values that do not change. const correctness is the practice of using the const keyword to prevent const objects from getting mutated.

Just make everything const that you can!

We should add const early and often. Back-patching const correctness results in a snowball effect: every const over here requires four more to be added over there.

const member functions

You can declare a non-static member function const if it doesn’t change the value of the underlying object.

constexpr

constexpr variables

Let’s review the definition of a literal type. According to the standard:

A literal type is one whose layout can be determined at compile time. The following are the literal types:

  • void
  • Scalar types
  • References
  • Arrays of void, scalar types or references
  • A class that has a constexpr destructor, and one or more constexpr constructors that are not move or copy constructors. Additionally, all its non-static data members and base classes must be literal types and not volatile.

A const variable is immutable. The initialization of a const variable may be deferred to run-time. A constexpr variable must be initialized with a value that is known at compile-time. All constexpr variables are const, but the converse is not true.

constexpr float c = 3.0e8;
constexpr float G = 6.67430e-11;

constexpr functions

A constexpr function foo() is one that may be evaluated at compile-time or run-time. If a function marked constexpr is called in a constant expression context, only then, the function is evaluted at compile-time. We have roughly four options for doing so:

  • Assign the result of foo to a constexpr variable.
  • Use foo as a non-type template argument.
  • Use foo as the size of the array.
  • Call foo in another constexpr function that is evaluated in a constant evaluation.

Here are the four cases in code:

constexpr std::size_t double_up(std::size_t n){
    return 2 * n;
}

constexpr std::size_t other(std::size_t n){
    return double_up(n);
}

constexpr std::size_t result = double_up(5);
std::array<int, double_up(3)> a{1, 2, 3, 4, 5, 6};
char buffer[double_up(10)];
constexpr auto value{other(4)};

if constexpr

if constexpr is a constexpr if statement where the predicate is evaluated is compile-time. It can be used to modify the behavior of the enclosing function. So, the resulting binary will include one branch and not the other. Generally, you wouldn’t use this outside of a template function.

#include <cmath>
#include <optional>

template<typename T>
bool isEqual(T a, T b, std::optional<double> epsilon = 1e-7){
    if constexpr(std::is_same_v<T, float> || std::is_same_v<T, double>){
        return std::abs(a - b) < epsilon;
    }else{
        return a == b;
    }
}

Compiler Explorer

consteval functions

A function marked consteval is called an immediate function. Immediate here means that the funtion is evaluated at the compiler front-end, yielding only a value that the compiler back-end uses. Such a function never goes into your binary. A consteval-function must be evaluated at compile-time or compilation fails.

A call to a consteval function must be a constant-expression.

consteval int sqr(int n){
    return n * n;
}

constexpr int r = sqr(100); // Okay
int x = 100;
// int r2 = sqr(x); //Error: Not a constant expression

There is one exception to the rule that a call to a consteval function must be a constant-expression: if the call appears in another consteval function, it need not be a constant-expression (since the call to the enclosing function would ultimately have to produce a core constant expression).

consteval int sqrsqr(int n) {
  return sqr(sqr(n)); // Not a constant-expression at this  point,
}                     // but that's okay.

//constexpr int dblsqr(int n) {
//  return 2*sqr(n); // Error: Enclosing function is not
//}    

std::is_constant_evaluated()

std::is_constant_evaluated() is a magic library function to check if the current evaluation context is constant evaluation.

Despite the fact that support for both consteval functions and std::is_constant_evaluation were introduced in C++20, these features interact poorly.

if consteval

Interaction between constexpr and consteval

Consider the interplay between the magic library function std::is_constant_evaluated() and the new consteval. Consider the following example:

consteval int f(int i) { return i; }

// The constexpr function g() does not compile
// constexpr int g(int i) {
//     if (std::is_constant_evaluated()) {
//         return f(i) + 1; // <==
//     } else {
//         return 42;
//     }
// }

consteval int h(int i) {
    return f(i) + 1;
}

Compiler Explorer

The function h here is basically a lifted, constant-evaluation-only version of the function g. At constant evaluation time, they do the same thing, except that during runtime, you cannot call h, and g has this extra path. Maybe this code started with just h and someone decided a runtime version would also be useful and turned it into g.

Unfortunately, h is well-formed, while g is ill-formed. The call to f() inside g() is an immediate invocation. You cannot make a call to f from inside of g (the location marked with an arrow). During the static analysis phase, the compiler front-end requires that the call to f() inside g(), an immediate invocation, must be a constant expression and it is not.

The if constexpr(std::is_constant_evaluated()) problem

What happens, if we modify the simple if statement to if constexpr? Consider the below code: Because the predicate of if constexpr is evaluated at compile-time, if constexpr(std::is_constant_evaluated()) is trivially true and is buggy code.

constexpr int foo(int i) {
    if constexpr (std::is_constant_evaluated()) {
        /* ... */
    } else {
        return 42;
    }
}

The if consteval statment

The if consteval is exactly the same as

if(std::is_constant_evaluated())

with one key difference: we can use if consteval to invoke immediate functions. According to the standard:

An expression or conversion is in an immediate function context if it is potentially evaluated and its innermost non-block scope is a function parameter scope of an immediate function. An expression or conversion is an immediate invocation if it is an explicit or implicit invocation of an immediate function and is not in an immediate function context. An immediate invocation shall be a constant expression.

consteval int f(int i) { return i; }

constexpr int g(int i) {
    if consteval {
        return f(i) + 1; // ok: immediate function context
    } else {
        return 42;
    }
}

consteval int h(int i) {
    return f(i) + 1; // ok: immediate function context
}

Compiler Explorer

Challenge puzzle

Which line should we remove to make this program compile successfully?

constexpr int second(int c){
    static_assert(c > 0);
}

constexpr int first(int b){
    int result = second(b);
    return result;
}

int main(){
    constexpr int a = first(4);
    std::cout << a;
}
    1. std::cout << x;
    1. static_assert(c > 0);
    1. constexpr int n = first(4);
    1. int result = second(b);

For more such puzzles, visit getcracked.io

constinit keyword

constinit keyword can be used to force and ensure that a mutable static or global variable is initialized at compile-time. So, roughly speaking the effect is described as :

constinit = constexpr - const

A constinit variable is not const. The keyword would be better named compileinit or something.

You can use constinit whenever you declare a static or a global variable. For example,

constinit auto i{42};

int incrementCounter(){
    static constinit int counter{0};
    return ++counter;
}

constexpr std::array<int,5> getCollection(){
    return {1, 2, 3, 4, 5};
}

constinit auto globalCollection = getCollection();

You can still modify the declared values. The effect of using constinit is that the initialization only compiles if the initial value is known at compile-time.

There are a couple of things to repect when using constinit in practice.

First, you cannot initialize a constinit value with another constinit value:

constinit auto x = f(); //f() must be constexpr
constinit auto y = x;   // Error: x is not a constant initializer

The reason is that the initial value must be a constant value known at compile time, but constinit values are not constant. So, x is open for modification at run-time, prior to assigning it to y. The next code snip compiles fine:

constexpr auto x = f(); //f() must be a compile-time function
constinit auto y = x;   // ok

When initializing objects, a compile-time constructor is required. However, a compile-time destructor is not required.

constinit does not imply inline (this is different from constexpr).

How constinit solves the static initialization order fiasco?

In C++, there is a problem called the static initialization order fiasco, which constinit can solve. The problem is that the order of static and global initializations in different translation units is not defined.

Assume that we have a type with a constructor to initialize the objects and introduce an extern global object of this type:

References

  • const correctness, C++ core guidelines.
  • C++ 20 - The complete guide, by Nikolai Josuttis.