Introduction
Early on, const was added to C++. (It was later back-ported to C.) There are three uses for const:
-
constobjects. - Pointers or references to
const. -
constmember 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”:
Whether it’s initialized at compile-time (vs. run-time) — constant initialization.
Whether it’s immutable after being initialized — 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
constobjects, except in C,constisexternby 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 eitherstaticorextern.
For example:
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:
const int WEST = 180;
int const EAST = 0; // Same as: const int.
The latter style is known as east const 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:
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:
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:
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
constreferred to as “const pointers” or “const references” (respectively). The problem is that, for pointers, “const pointer” technically means the pointer isconst, not the pointed-to object. (Note that “const reference” doesn’t have this problem because all references areconst, 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:
class C {
// ...
void f();
void fc() const;
};
Then:
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(andconstimpliesstatic).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.
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-
constexprfunctions.
For example:
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:
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()returningintqualified withconstis legal, it’s pointless. It’s done here only to illustrate another difference betweenconstandconstexpr.
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:
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
constexprfunctions necessary? Why can’t an ordinaryinlinefunction 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:
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:
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:
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:
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:
// file.h
extern constinit int MAX;
// file.cpp
constinit int MAX = 42;
constinit was added to help avoid the static initialization order fiasco. For example, given:
// 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:
// 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:
// 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:
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:
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:
// 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. 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”:
-
constexpr. -
constinit(but only if mutability is needed). -
const.
For inline functions, either non-member or static member:
-
consteval. -
constexpr.
For inline, non-static member functions:
-
constconsteval. -
constconstexpr. -
consteval. -
constexpr.
Summary
To summarize, consider this diagram:
First, there are two categories of “const-ness”: for objects and for functions. For objects:
constis immutable and may be either statically or dynamically initialized.constexpris also immutable and must be statically initialized.constinitis mutable and must be statically initialized.
For functions:
constonly applies to member functions and their objects are immutable.inlinefunctions are always evaluated at run-time.constexprfunctions are evaluated at compile-time only if all their arguments are constant expressions; otherwise they are evaluated at run-time.constevalfunctions are always evaluated at compile-time.

Top comments (1)
very insightful