C++ Const Conundrum
Tags: cpp
Published: 2023-08-30 15:59:38

---
title: C++ Const Conundrum
published: true
description: Descriptions of const, constexpr, consteval, and constinit in C++, and when to use which.
tags: #cpp
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-08-30 01:24 +0000
---

## Introduction

Early on, `const` was added to C++. (It was later back-ported to C.) There are three uses for `const`:

1. `const` objects.
2. Pointers or references to `const`.
3. `const` member functions.

None are particularly complicated. However, `constexpr` was added in C++11 and both `consteval` and `constinit` were added in C++20. Knowing which of the four to use in a particular case can be quite the conundrum.

## `const`

As a refresher, here are details for the use of `const`.

### `const` Objects

For objects, there are two parts to “const-ness”:

1. Whether it’s initialized at compile-time (vs. run-time) &mdash; _constant initialization_.

2. Whether it’s immutable after being initialized &mdash; _immutability_.

An object declared `const`:

+ Means _only_ #2 above: it’s immutable.

+ Must be explicitly initialized in its definition.

+ May be initialized with either a constant (compile-time) or dynamic (run-time) expression.

+ If initialized with a constant expression, it’s typically initialized at compile-time, but it’s not _guaranteed_ to be by the implementation.

+ Is `static` (has internal linkage) by default.

+ May be explicitly declared `extern` (have external linkage).

> C also has `const` objects, except in C, `const` is `extern` by default. If compatibility with C is required or you just want to be clear and not have to remember what the default linkage is in which language, always explicitly specify either `static` or `extern`.

For example:

```cpp
int const ANSWER = 42;      // Initialized at compile-time.
int const SEED   = rand();  // Initialized at run-time.

// file.h
extern int const MAX;       // Declaration.

// file.cpp
int const MAX = 10;         // Definition.
```

Note that the compiler doesn’t care where you put `const`:

```cpp
const int WEST = 180;
int const EAST = 0;  // Same as: const int.
```

The latter style is known as [east const](https://mariusbancila.ro/blog/2018/11/23/join-the-east-const-revolution/) and it’s the style I prefer because then `const` is always making constant what’s to its immediate left (hence, the `const` is to the right or “east”). (This becomes relevant when pointers are involved.)

Attempting to modify a constant object results in [undefined behavior](https://dev.to/pauljlucas/undefined-behavior-in-c-and-c-3a20):

```cpp
int *p = const_cast<int*>(&MAX);
*p = 43;                    // Undefined behavior.
```

### Pointers or References to `const`

A pointer or reference to `const` can refer to either a `const` or non-`const` object. The `const` means that you can’t modify the referred-to object via _that_ pointer or reference. Whether the referred-to object is actually `const` is irrelevant:

```cpp
int i = 42;
int const *p = &i;  // pointer to const pointing at non-const
*p = 43;            // error: can't modify via pointer to const
```

Note that the pointer itself can also be `const`:

```cpp
int *const cp = &i;         // constant pointer to non-const
int const *const cpc = &i;  // constant pointer to const
```

For a constant pointer, the `const` _must_ appear to the right (“east”) of the `*`.

> You’ll often read or hear pointers or references to `const` referred to as “const pointers” or “const references” (respectively). The problem is that, for pointers, “const pointer” technically means the _pointer_ is `const`, _not_ the pointed-to object. (Note that “const reference” doesn’t have this problem because all references are `const`, hence “const reference” always means “reference to const.”) Therefore to be precise for pointers, I’ll use “pointer to const” unless I really mean “constant pointer.” (I’ll also use “reference to const” for symmetry.)

### `const` Member Functions

Marking a member function as `const` means it can be called on `const` objects (or via pointers or references to `const` objects). Given:

```cpp
class C {
    // ...
    void f();
    void fc() const;
};
```

Then:

```cpp
C c;
C const cc;

c.f();    // OK.
c.fc();   // OK.
cc.f();   // Error: non-const f() called on const object.
cc.fc();  // OK.

C *p = &c;
C const *pc = &c;  // Not a typo.

p->f();   // OK.
p->fc();  // OK.
pc->f();  // Error: non-const f() called via pointer to const.
pc->fc(); // OK.
```

## `constexpr`

C++11 added `constexpr` that’s a “const-er” `const` for both objects and functions.

### `constexpr` Objects

An object declared `constexpr`, like `const`:

+ Must be explicitly initialized in its definition.

However, unlike `const`:

+ Is always initialized at compile-time.

+ Must be initialized with a constant expression.

+ Implies `const` (and `const` implies `static`).

+ May _not_ be explicitly declared `extern` (external linkage).

+ Is itself a constant expression.

A `constexpr` object can be used in places where constant expressions are required such as array dimensions and `case` labels.

```cpp
constexpr int N = 42;      // OK.
constexpr int R = rand();  // Error.
```

### `constexpr` Functions

A function declared `constexpr`:

+ Implies `inline`.

However, unlike `inline`:

+ Will be evaluated at compile-time (but only if all arguments are constant expressions).

+ Can be used to initialize objects declared `constexpr` (when evaluated at compile-time).

+ May not call non-`constexpr` functions.

For example:

```cpp
constexpr auto max( auto i, auto j ) {
    return i > j ? i : j;
}

constexpr int N = max( 1, 2 );  // Compile-time.
```

Note that `constexpr` is a _specifier_ (like `extern`, `static`, `thread_local`, etc.) whereas `const` is a _qualifier_:

```cpp
const int f();      // const applies to int.
constexpr int g();  // constexpr applies to g(), not int.
static int h();     // Just like static applies to h().
```

> While `f()` returning `int` qualified with `const` is legal, it’s pointless. It’s done here only to illustrate another difference between `const` and `constexpr`.

Perhaps curiously, `constexpr` does not _require_ functions to be evaluated at compile-time. If you supply at least one non-constant argument, the function will be evaluated at run-time:

```cpp
int a = 1, b = 2;

constexpr int M1 = max( 1, 2 );  // OK: compile-time.
const     int M2 = max( a, b );  // OK: run-time.
constexpr int M3 = max( a, b );  // Error: not a constant expression.
```

Hence, a `constexpr` function will be evaluated at compile-time only if it can be.

### `constexpr` Function Rationale

At this point, you might ask:

> Why are `constexpr` functions necessary?  Why can’t an ordinary `inline` function given only constant expression arguments result in a constant expression?  Why can’t the compiler just figure it out?

It _could_, but the reason it doesn’t is that a `constexpr` function _forbids_ calling non-`constexpr` functions:

```cpp
inline int r1( int n ) {
    return n * rand();  // OK.
}

constexpr int r2( int n ) {
    return n * rand();  // Error: call non-constexpr function.
}
```

This is good because it prevents you from accidentally introducing a call to a non-`constexpr` function resulting in the function as a whole silently changing from a constant expression to a non-constant expression. Specifying `constexpr` is a statement of intent to the compiler that you want its help to keep a function a constant expression.

## `constexpr` Constructors & Member Functions

In addition to functions being `constexpr`, constructors can also be `constexpr` that allows entire class objects to be initialized at compile-time:

```cpp
class C {
public:
    constexpr C( int n ) : _n{ n } { }
private:
    int _n;
};

constexpr C c{ 42 };  // Constructed at compile-time.
```

Member functions can also be `constexpr` just like non-member functions; `constexpr` member functions can also be `const`:

```cpp
class C {
public:
    int f();
    int f() const;      // Can overload by const.

    constexpr int g();
    constexpr int g() const;

    int h();
    constexpr int h();  // Error: can't overload by constexpr.
};
```

## `consteval`

C++20 added `consteval` that’s an even “const-er” `constexpr`, but only for functions.

A function declared `consteval`, like `constexpr`:

+ Can be used to initialize objects declared `constexpr`.

However, unlike `constexpr`:

+ All arguments _must_ be constant expressions.

+ Will _always_ be evaluated at compile-time.

+ Is known as an _immediate function_.

+ _No_ code is generated for it; therefore you can’t have a pointer or reference to it.

Some examples of the differences between `constexpr` and `consteval`:

```cpp
constexpr auto max_expr( auto i, auto j ) {
    return i > j ? i : j;
}

consteval auto max_eval( auto i, auto j ) {
    return i > j ? i : j;
}

int a = 1, b = 2;

constexpr int M1 = max_expr( 1, 2 );  // OK: compile-time.
const     int M2 = max_expr( a, b );  // OK: run-time.

constexpr int M3 = max_eval( 1, 2 );  // OK: compile-time.
const     int M4 = max_eval( 1, 2 );  // OK: compile-time.
const     int M5 = max_eval( a, b );  // Error: can't be compile-time.
```

## `constinit`

C++20 also added `constinit` that requires constant initialization, but only for global, `static`, or `thread_local` objects (no non-`static` local objects nor non-`static` data members), nor functions.

An object declared `constinit`, like `const`:

+ Must be explicitly initialized in its definition.

+ May be explicitly declared `extern`.

However, unlike `const`:

+ Is _always_ initialized at compile-time.

+ Must be initialized with a constant expression.

+ Is _not_ immutable by default.

+ Is _not_ a constant expression.

A `constinit` declaration need not be a definition:

```cpp
// file.h
extern constinit int MAX;

// file.cpp
constinit int MAX = 42;
```

`constinit` was added to help avoid the [static initialization order fiasco](https://isocpp.org/wiki/faq/ctors#static-init-order). For example, given:

```cpp
// file1.cpp
int square( int n ) {
    return n * n;
}

int C1 = square( 5 );

// file2.cpp
extern int C1;
int C2 = C1;        // C2 can be either 0 or 25.
```

If the variables in `file1.cpp` are dynamically initialized before those in `file2.cpp`, then `C2` will be 25; however, if the variables in `file2.cpp` are dynamically initialized before those in `file1.cpp`, then `C2` will be 0.

There’s a 50:50 chance of which value `C2` will get. This is determined at compile-time, so multiple runs of the program will always produce the same value consistently. However, one day, you might change the program in a seemingly inconsequential way, change the object file link order, change the compiler options, or do something else that will cause the variables to be initialized in the wrong order and the program will break.  Initialization dependency order bugs are _very_ hard to find.

There is _no way_ to force a specific order of dynamic initialization. However, there is a work-around:

```cpp
// file1.cpp
int square( int n ) {
    return n * n;
}

int& get_C1() {
    static int C1 = square( 5 );
    return C1;
}

// file2.cpp
int& get_C1();
int C2 = get_C1();  // C2 is always 25.
```

This makes `C1` local to a function. C++ guarantees that `static` local variables are initialized before their first use. So even if the variables in `file2.cpp` are initialized first, it doesn’t matter because `C1` will be initialized regardless prior to `get_C1()` returning.

With `constinit`, you don’t need the work-around because it guarantees `C1` will be initialized at compile-time:

```cpp
// file1.cpp
consteval int square( int n ) {
    return n * n;
}

constinit int C1 = square( 5 );

// file2.cpp
extern int C1;
int C2 = C1;        // C2 is always 25.
```

## Miscellaneous

The following are miscellaneous declarations. The first is of the form:

```cpp
constexpr const int CECI = 42;  // const is redundant.
```

Since `constexpr` implies `const`, adding `const` to a `constexpr` is pointless.

The second is of the form:

```cpp
constinit const int CICI = 42;  // Should likely be constexpr.
```

A statically initialized, immutable object should be `constexpr` instead, if possible. However, one use-case for a `constinit` `const` would be if you wanted to have an immutable object that’s statically initialized by a non-trivial `constexpr` or `consteval` function that you do _not_ want to put into a header:

```cpp
// file.h
extern constinit const int CICI;

// file.cpp
constexpr int calc_CICI() {
    // ...
}

constinit const int CICI = calc_CICI();
```

## “Const Correctness”

You’ve likely heard the term [const correctness](https://isocpp.org/wiki/faq/const-correctness).  Historically, this involved only `const`.  With the additions of `constexpr`, `consteval`, and `constinit`, does the meaning of `const` correctness change?

Yes.  Objects should be in order of preference from most-to-least “const”:

1. `constexpr`.
2. `constinit` (but only if mutability is needed).
3. `const`.

For `inline` functions, either non-member or `static` member:

1. `consteval`.
2. `constexpr`.

For `inline`, non-`static` member functions:

1. `const` `consteval`.
2. `const` `constexpr`.
3. `consteval`.
4. `constexpr`.

## Summary

To summarize, consider this diagram:

![Const Venn Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/whd6zelesd94vtkfb94d.png)

First, there are two categories of “const-ness”: for objects and for functions.  For objects:

+ `const` is immutable and may be either statically or dynamically initialized.

+ `constexpr` is also immutable and _must_ be statically initialized.

+ `constinit` is _mutable_ and _must_ be statically initialized.

For functions:

+ `const` only applies to member functions and their objects are immutable.

+ `inline` functions are always evaluated at run-time.

+ `constexpr` functions are evaluated at compile-time only if all their arguments are constant expressions; otherwise they are evaluated at run-time.

+ `consteval` functions are _always_ evaluated at compile-time.