C++ / OPERATORS AND EXPRESSIONS
const and constexpr variables for named constants
Choose between const and constexpr for named constants, and know exactly which one you can use as an array bound, case label, or static_assert operand.
What you will learn
- Declare read-only values with const; its initializer may be computed at run time.
- Use constexpr when the value must exist while compiling: array bounds, case labels.
- Know that constexpr on a variable already implies const, so writing both is redundant.
- Lock derived constants down with static_assert so a bad edit fails the build.
Understanding const and constexpr variables for named constants
const answers a permission question: after initialization, this name may not be used to write to the object. The initializer itself can be anything the surrounding code is able to compute, including a function call or a value read from input, and it is evaluated when control reaches the declaration. That is also why a const object must be given its value right there, since no later assignment is possible.
constexpr answers a timing question instead: the initializer must be a constant expression, so the value is produced while the compiler is still running. Only that guarantee lets a name appear where the language grammatically demands a compile-time constant, such as an array bound, a case label, a static_assert condition, or a template argument. On a variable, constexpr also implies const, because a value baked into the program at compile time cannot be reassigned.
The two get confused because of one rule kept from early C++: a const object of integral or enumeration type initialized with a constant expression may itself be used in constant expressions. So const int n = 10; int a[n]; compiles, and readers conclude that const means compile-time. Change the type to double, or initialize from an ordinary function call, and the identical shape of code stops compiling. Writing constexpr states the requirement at the declaration, where the error is easy to read, instead of at some distant use.
Prefer constexpr for any value the source text fully determines, and reach for const when the value genuinely depends on something the program learns while running.
<iostream>
constexpr int kTileSize = 16;
constexpr int kTilesPerRow = 4;
constexpr int kRowWidth = kTileSize * kTilesPerRow; // folded while compiling
int tilesFromConfig() { return 3; } // plain function: result exists only at run time
int main() {
static_assert(kRowWidth == 64, "row width must stay 64");
int pixels[kRowWidth] = {}; // the bound needs a compile-time constant
pixels[kRowWidth - 1] = 255;
const int tiles = tilesFromConfig(); // read-only, but fixed only at run time
// tiles = 4; // error: assignment of read-only variable 'tiles'
std::cout << "row width: " << kRowWidth << '\n';
std::cout << "array size: " << sizeof pixels / sizeof pixels[0] << '\n';
std::cout << "last pixel: " << pixels[kRowWidth - 1] << '\n';
std::cout << "tiles: " << tiles << '\n';
}
const controls whether a name may be written through, while constexpr controls whether its value exists during compilation, and only constexpr is dependable where a compile-time constant is required.
Worked examples
The const integral exemption, and where it stops
Shows that a const int with a literal initializer works as an array bound, while a const double is unusable in a constant expression.
<iostream>
int main() {
const int size = 4; // const + integral + constant initializer
constexpr int scale = 3;
int a[size]; // accepted: size is a constant expression
for (int i = 0; i < size; ++i) a[i] = i * scale;
const double ratio = 1.5;
// static_assert(ratio > 1.0, ""); // error: ratio is not usable in a constant expression
constexpr double kRatio = 1.5;
static_assert(kRatio > 1.0, "ratio must stay above 1");
std::cout << a[3] << ' ' << kRatio * a[3] << '\n';
std::cout << ratio * a[1] << '\n';
}
Example explained
Line 1int a[size] compiles because size is a const object of integral type with a constant initializer, which the standard permits inside constant expressions.
Line 2The commented static_assert would fail: the same courtesy is not extended to floating-point types, so a const double stays a run-time value.
Line 3constexpr double kRatio is a compile-time value, so the comparison against 1.0 is checked before the program runs.
Line 4kRatio * a[3] mixes a compile-time constant with an array element, so that multiplication still happens at run time.
const for run-time values, constexpr for compile-time ones
Demonstrates which declarations survive when the initializer comes from an ordinary function call.
<iostream>
int rowsFromFile() { return 2; } // not constexpr
int main() {
constexpr int cols = 5;
const int rows = rowsFromFile(); // fine: const only forbids later writes
// constexpr int total = rows * cols; // error: rows has no compile-time value
const int total = rows * cols; // evaluated once, then frozen
int flat[cols];
for (int i = 0; i < cols; ++i) flat[i] = i * i;
// rows = 3; // error: assignment of read-only variable 'rows'
std::cout << "rows " << rows << ", total " << total << '\n';
std::cout << "flat[4] " << flat[4] << '\n';
}
Example explained
Line 1rowsFromFile is a normal function, so its result is only available once the program is executing.
Line 2const int rows accepts that result; marking it constexpr would be rejected as a call to a non-constexpr function.
Line 3total is const but not constexpr, so it can hold a derived run-time value yet cannot size an array.
Line 4int flat[cols] works because cols traces back to constexpr, which never depends on run-time state.
Important notes
On a pointer, constexpr freezes the pointer itself: constexpr char* p = buf; means char* const p, so writing through *p is still allowed. Use const char* to protect the target.
constexpr std::string s = "hi"; does not compile even in C++20, because the string's allocation cannot outlive constant evaluation; use constexpr std::string_view for a compile-time text constant.
Common mistakes
Writing constexpr int n = readSize(); the compiler rejects the declaration outright with a message about calling a non-constexpr function instead of quietly falling back to run time; const is the correct keyword there.
Assuming const double ratio = 1.5; can size an array or feed a static_assert because const int can; only integral and enumeration types get that exemption, and the error surfaces at the use site rather than the declaration.
Declaring const int limit; with no initializer, which fails immediately as an uninitialized const, since nothing can ever assign to it afterwards.
Try it yourself
Change, predict, then run
Declare constexpr int kWidth = 320; and constexpr int kHeight = 200;, derive constexpr int kPixels = kWidth * kHeight;, assert static_assert(kPixels == 64000, "resolution changed"); and print kPixels. Then change kHeight to 201 and confirm the failure is reported at the static_assert, before the program ever runs.
Open the C++ workspaceCheck your understanding
Why does const int n = 10; int a[n]; compile, while const double r = 1.5; static_assert(r > 1.0, ""); does not?
- static_assert only accepts integer conditions, so a double comparison can never appear in one.
- A const object of integral or enumeration type with a constant initializer is usable in constant expressions, and that rule does not extend to floating-point types.
- double cannot be const-qualified, so r is treated as an ordinary mutable variable.
- Array bounds are resolved at run time, whereas static_assert is checked while compiling.
Show answer
The integral case is a special rule carried over from early C++, which is why n works as an array bound while an otherwise identical double does not; adding constexpr to r makes the assertion compile. The last option is tempting but wrong: in standard C++ an array bound must also be a constant expression, so nothing about int a[n] is deferred to run time.