Introduction
Early on, const
was added to C++. (It was later back-ported to C.) There are three uses for const
:
-
const
objects. - Pointers or references to
const
. -
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”:
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
const
objects, except in C,const
isextern
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 eitherstatic
orextern
.
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
const
referred 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
(andconst
impliesstatic
).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-
constexpr
functions.
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()
returningint
qualified withconst
is legal, it’s pointless. It’s done here only to illustrate another difference betweenconst
andconstexpr
.
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
constexpr
functions necessary? Why can’t an ordinaryinline
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:
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:
-
const
consteval
. -
const
constexpr
. -
consteval
. -
constexpr
.
Summary
To summarize, consider this diagram:
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.