Guide to constexpr in C23 for Immutable Values
The C23 revision of the ISO C standard introduces the
constexpr specifier, providing a standardized mechanism to
declare named, type-safe, compile-time constant objects. This article
examines how constexpr operates in C23, how it differs from
traditional const variables and #define
preprocessor macros, its syntax and initialization rules, and the
practical advantages it brings to modern C development.
The Purpose of constexpr in C23
Historically, the C language lacked a way to define a strictly typed,
scoped, compile-time constant. Developers relied on #define
preprocessor macros or enum declarations to establish
values known at translation time.
The standard const qualifier in C merely designates an
identifier as "read-only" at runtime. A const variable does
not constitute a constant expression under standard C rules;
consequently, it cannot reliably be used for array dimensions in non-VLA
(Variable Length Array) contexts, case labels in
switch statements, or static assertions.
C23 resolves this limitation by adopting the constexpr
specifier for object declarations. Unlike C++, C23 limits
constexpr strictly to object declarations—it cannot be
applied to functions.
Defining Immutable Values with constexpr
When an object is declared with constexpr, it becomes an
immutable value evaluated strictly at compile time.
#include <stdio.h>
constexpr int MAX_BUFFER = 1024;
constexpr double PI = 3.141592653589793;
int main(void) {
// Used as a standard integer constant expression
int buffer[MAX_BUFFER];
switch (getchar()) {
case MAX_BUFFER: // Valid in C23
break;
default:
break;
}
return 0;
}Because MAX_BUFFER is a genuine constant expression, the
array buffer is a fixed-size array rather than a VLA,
ensuring predictable stack allocation.
Key Rules and Semantics
The constexpr specifier enforces several constraints to
guarantee that the declared object remains fully immutable and
resolvable during compilation:
- Mandatory Initializer: Every
constexprobject must be defined with an explicit initializer. Tentative or uninitialized declarations are forbidden. - Constant Expression Requirement: The initializer must evaluate to a translation-time constant expression. It cannot depend on runtime variables, dynamic function calls, or side effects.
- Implicit
constQualification: Applyingconstexprto an object automatically imparts theconstqualifier to its type. Attempting to modify aconstexprobject results in a compilation error. - Scope and Storage: Unlike preprocessor macros,
constexprsymbols respect lexical scoping rules. Storage for aconstexprobject is only allocated if the program explicitly takes its address (e.g.,&MAX_BUFFER), allowing the compiler to optimize the symbol away entirely in standard evaluation paths. - Composite Types: Structures and arrays can be
declared with
constexpr, provided all members or elements are initialized with constant expressions:
struct Point {
int x;
int y;
};
constexpr struct Point ORIGIN = { .x = 0, .y = 0 };
constexpr int MATRIX[2][2] = { {1, 0}, {0, 1} };constexpr vs. const vs. #define
| Feature | #define |
const |
constexpr (C23) |
|---|---|---|---|
| Evaluation Time | Preprocessing phase | Runtime (read-only) | Compile time |
| Type Safety | None (textual replacement) | Full | Full |
| Scope Rules | Global (file-level) | Block / File scope | Block / File scope |
Usable in case
labels |
Yes | No | Yes |
Usable in
_Static_assert |
Yes | No | Yes |
| Debugger Friendly | Poor (often stripped) | Good | Good |
Summary
The C23 constexpr specifier bridges a long-standing gap
in the C language by unifying the type safety and scoping of
const with the compile-time evaluation guarantees of
preprocessor macros. By utilizing constexpr, developers can
write safer, cleaner, and more optimizable code without depending on
untyped #define directives for compile-time constants.