Introduction
Originally, const
was not part of C. (C++ added const
and it was later back-ported to C89.) In C, there are two uses for const
:
-
const
objects. - Pointers to
const
.
Neither is particularly complicated. However, constexpr
was added in C23. Knowing which of the two to use in a particular case can be quite the conundrum.
If this article seems like déjà vu, it’s because I previously wrote about a C++ Const Conundrum. However, this article is about C.
const
As a refresher, here are details for the use of const
in C. There are two parts to “const-ness”:
Whether it’s initialized at compile-time (vs. run-time) — constant initialization.
It’s immutable after being initialized — immutability.
An object declared const
:
Always means #2 above: it’s immutable.
Must be explicitly initialized in its definition.
May be initialized with a constant (compile-time) expression.
However, unlike C++, objects with static storage duration (those either at global scope or explicitly declared
static
), must be initialized with a constant expression.Objects with dynamic storage duration (non-
static
, function-local objects) may be initialized with a dynamic (run-time) expression.For global objects, is
extern
(has external linkage) by default.May be explicitly declared
static
(have internal linkage).
C++ also has
const
objects, except in C++,const
for global objects isstatic
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:
// file.h
extern int const MAX; // Declaration.
// file.cpp
int const MAX = 10; // Definition.
int const ANSWER = 42; // Initialized at compile-time.
int const SEED = rand(); // Error in C; OK in C++.
int main() {
int const SEED = rand(); // OK in both C and C++.
// ...
}
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 = (int*)&MAX;
*p = 43; // Undefined behavior.
Pointers to const
A pointer to const
can point to either a const
or non-const
object. The const
means that you can’t modify the pointed-to object via that pointer. Whether the pointed-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 to
const
referred to as “const pointers.” The problem is that “const pointer” technically means the pointer isconst
, not the pointed-to object. Therefore to be precise, I’ll use “pointer to const” unless I really mean “constant pointer.”
const
Isn’t Quite Constant
You might think that anything declared const
is, well, constant. The problem is that in some cases, the compiler treats const
like it isn’t really constant.
The first case is that a const
integer can not be used as a case
label:
void f( int n ) {
int const MAX = 10;
switch ( n ) {
case MAX: // Error.
// ...
Fortunately, you don’t run into this case that often. However, the second, more common, and surprising case is that a const
integer can not be used to specify an array’s size:
int const MAX = 10;
int array[MAX]; // Error.
Note that some compilers will accept the above code as an extension.
Inside a function, such code will be accepted, but for a different reason:
void f( void ) {
int const MAX = 10;
int array[MAX]; // OK, but VLA.
// ...
}
The code is accepted not because the compiler considers MAX
to be constant, but because array
is a variable length array — something you probably don’t want.
The traditional work-arounds for lack of true “const-ness” for const
are either to use a macro or an enumeration:
#define MAX1 10
int array1[MAX1]; // OK.
enum { MAX2 = 10 };
int array2[MAX2]; // OK.
Being forced to use either because const
isn’t truly constant is just deficient. This is where constexpr
comes in.
constexpr
C23 adopted constexpr
from C++ that’s a “const-er” const
for 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.
- May not be explicitly declared
extern
(external linkage). - Is itself a constant expression, i.e., is truly constant.
A constexpr
object can be used in places where constant expressions are required such as the aforementioned case
labels and array dimensions:
constexpr int MAX3 = 10;
int array3[MAX3]; // OK in C23.
Why didn’t the C Committee just “fix” const
instead, i.e., make it truly constant in all cases? Because they didn’t want to change the meaning of existing code nor increase the existing incompatibility of const
between C and C++.
No constexpr
Functions
In C++, constexpr
can also be used for functions; however, that ability was not adopted into C. (Perhaps it will be in a future standard revision.)
Miscellaneous
Since a constexpr
object is truly constant, adding const
is pointless:
constexpr const int CECI = 42; // const is pointless
“Const Correctness”
You’ve likely heard the term const correctness. Historically, this involved only const
. With the addition of constexpr
, does the meaning of const
correctness change?
Yes. Generally, constexpr
should be preferred to const
. Use const
only when you need dynamic initialization.
Conclusion
With the adoption of constexpr
for objects, C finally fixed a long-standing language deficiency. Use constexpr
whenever possible.