const, constexpr, consteval and constinit

C++
Author

Quasar

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 functions

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

In case a function has two overloaded versions where one is const and the other is not, the compiler will choose which one to call based on whether the object itself is const or not.

#include <iostream>
#include <initializer_list>
#include <print>

struct Point{
    double m_x;
    double m_y;

    explicit Point(double x, double y)
    : m_x{ x }
    , m_y{ y }
    {}

    double& x(){ 
        std::println("double& x()");
        std::println("x = {}", m_x);
        return m_x; 
    }
    const double& x() const{ 
        std::println("const double& x() const"); 
        std::println("x = {}", m_x);
        return m_x; 
    }
    double& y(){ 
        std::println("double& y()");
        std::println("y = {}", m_y);
        return m_y; 
    }
    const double& y() const{ 
        std::println("const double& y() const");
        std::println("y = {}", m_y);
        return m_y; 
    }
};

void print(const Point& p){

}
int main(){
    Point p(3.0, 4.0);
    p.x();
    p.y();

    const Point p2(1.0, -1.0);
    p2.x();
    p2.y();
    return 0;
}

Compiler Explorer

Output:

double& x()
x = 3
double& y()
y = 4
const double& x() const
x = 1
const double& y() const
y = -1

const variables

If you add the const qualifier to the type of a local variable, the local variable is immutable. Declaring variables as const also helps the compiler to perform some optimizations.

A member of a const-qualified struct or union type acquires the qualification of the type it belongs to.

struct S{
    int i; 
    const int ci;
};

const S cs;
// the types of cs.i and cs.ci are 
// both const int

If an array type is declared with the const type qualifier, it’s elements are considered const.

const A a[6] = {1, 2, 3, 4, 5, 6};

// int* pi = a[0]; // a[0] is const int*

A pointer to a non-const type can be implicitly converted to a pointer to const-qualified version of the same. The reverse conversion requires a cast expression.

const members

Having const local variables is good. Having const as a member variable is never a good idea. You cannot copy assign or move assign to the objects of such a type.

const return types

When returning objects by value, it is not recommended to use the const qualifier. It would be misleading to return a value by const object. RVO(Return Value Optimization) only kicks in when you return an object by value without the const qualifier.

Imagine that we need the unit vectors \(\hat{i} = (1,0)\) and \(\hat{j} = (0,1)\) in the physics engine in our game. We might specify these vectors as:

const Point unit_vector_i(1,0);
const Point unit_vector_j(0,1);

When an object is declared const, all its subobjects are const. This aligns with our intuition. Being able to modify the coordinates of the basis vectors should be an illegal and makes no sense.

Hence, the x() and y() getter methods should have a const version, that operates on const Point objects and returns a constant reference.

const function parameters

Consider the following code:

void f(int);        // declaration of f(int)
void f(const int);  // re-declaration of f(int)
void f(int){ /*...*/ } // defintion of f(int)
void f(const int){ /*...*/ } //re-definition of f(int)

A function declaration tells the compiler the function’s signature and return type. One line 2, the constness of the function’s parameter type is ignored. Similarly, line 4 is a definition of the same function f(int), which will result in an error at link time. Multiple declarations are allowed, but only a single definition is permitted.

Meaning of const in function declarations

Not all const qualifications in function declarations are ignored. To quote from from the C++ standard,

const type-specifiers buried within a parameter type specification are significant and can be used to distinguish overloaded function declarations.

void f(const int* x);                  // 1
void f(const int& x);                  // 2
void f(std::unique_ptr<const int> x);  // 3
void f(int* x);                        // 4

In all of the above examples, the parameter x itself is never declared const. const is buried inside the parameter type, hence each has a declaration has a different parameter type, forming an overload set.

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.

The primary difference between const and constexpr variable is that the initialization of a const variable can be deferred to compile-time. A constexpr variable must be initialized at compile time. All constexpr variables are const, but the converse is not true.

  • A variable can be declared with constexpr, when it has a literal type and is initialized. If the initialization is performed by a constructor, the constructor must be declared as constexpr.

  • A reference may be declared as constexpr if and only if the referenced object is initialized by a constant expression.

  • All declarations of a constexpr variable or function must have constexpr specifier.

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

constexpr functions

A constexpr function is one whose return value is computable at compile time. The caller(consuming code) requires the return value at compile-time to initialize a constexpr variable, or to provide a non-type template argument. When a constexpr function is passed arguments known at compile-time, it produces function produces a compile-time constant. When called with non-constexpr arguments, or when its return value isn’t required at compile-time, it produces a value at run time like a regular function. This dual behavior saves you from having to write constexpr and non-constexpr versions of the same function. It follows that, you cannot specify constexpr on the variables in the parameter list of a constexpr function. Recall, constexpr variables must be initialized at compile-time.

A constexpr function or constructor is implicitly inline. The inline keyword suggests to the compiler to substitute the code within the function definition in place of each call to the function.

The following rules apply to constexpr functions:

  • A constexpr function must accept and return only literal types.
  • The body can be defined as =default or =delete. This also applies to constructors and destructors.
  • The body cannot contain try blocks.
#include <iostream>
#include <print>
#include <array>
#include <cstdlib>
#include <cmath>

// Point is a literal type
struct Point {
    double x { 0 };
    double y { 0 };

    constexpr int distance(const Point& other) const { 
        return sqrt(pow(x - other.x,2) + pow((y-other.y),2)); 
    }
};

// Line is not a literal type. 
// Its destructor is not constexpr.
struct Line{
    Point m_head;
    Point m_tail;

    ~Line(){}
};

int main(){
    constexpr Point p(3.0, 4.0);
    constexpr Point o(0.0, 0.0);
    constexpr double d = p.distance(o);
    std::cout << "Distance from origin to (3.0,4.0) = "
              << d << "\n";
    return 0;
}

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.