Constants (const, constexpr)
A constant is a value that cannot change after it is set. C++ gives you two
main tools for this —
const and constexpr — plus the
old C-style #define macro, which modern C++ strongly discourages.
const — Runtime Constants
const tells the compiler that, once initialized, this variable can
never be reassigned. The value can still be computed at runtime (for example,
from user input), but after initialization it is locked.
CPP
#include <iostream>
int main() {
const double taxRate = 0.08;
// taxRate = 0.10; // ERROR: assignment of read-only variable
int userInput;
std::cin >> userInput;
const int lockedValue = userInput; // fine — computed at runtime, then locked
std::cout << taxRate << " " << lockedValue << std::endl;
return 0;
}constexpr — Compile-Time Constants
constexpr goes a step further: it tells the compiler the value
must be computable **at compile time**. This enables the compiler to bake the
value directly into the generated code (no runtime computation at all), and it
allows the constant to be used in contexts that require a compile-time value,
like array sizes.
CPP
#include <iostream>
constexpr int square(int x) { return x * x; }
int main() {
constexpr double pi = 3.14159;
constexpr int arraySize = square(4); // computed at compile time: 16
int fixedArray[arraySize]; // legal — arraySize is a compile-time constant
std::cout << pi << " " << arraySize << std::endl;
return 0;
}const vs constexpr
Every
constexpr variable is implicitly const, but not every const variable is constexpr — a const variable initialized from a runtime value (like user input) cannot be constexpr. Prefer constexpr whenever the value is genuinely known at compile time; it enables more compiler optimizations and stricter checking.The Old Way: #define Macros
Before
const and constexpr existed, C programmers used
the preprocessor to define constants:
CPP
#define PI 3.14159 // preprocessor macro — a raw text substitution
int main() {
double area = PI * 2 * 2;
return 0;
}Why modern C++ avoids #define for constants
A
#define macro is a pure text substitution performed by the preprocessor before compilation even begins. This means it has no type (no type checking, no implicit conversions), no scope (it is visible everywhere after its definition, ignoring namespaces/classes/blocks), and it cannot be inspected by a debugger since it no longer exists after preprocessing. Bugs caused by macros are often confusing because error messages reference the substituted text, not the macro name. Always prefer const or constexpr.const with Pointers and References
const interacts with pointers in two distinct ways, often called
top-level and low-level const.
Declaration | Meaning |
|---|---|
const int* ptr | Low-level const: the pointed-to value cannot change through this pointer |
int* const ptr | Top-level const: the pointer itself cannot be reassigned to point elsewhere |
const int* const ptr | Both: neither the pointer nor the pointed-to value can change |
CPP
int value = 10; int other = 20; const int* ptrToConst = &value; // *ptrToConst = 5; // ERROR: cannot modify through this pointer ptrToConst = &other; // OK: the pointer itself can be reassigned int* const constPtr = &value; *constPtr = 5; // OK: can modify the value // constPtr = &other; // ERROR: cannot reassign the pointer itself
A
constreference parameter (const std::string& name) is the standard way to pass large objects efficiently without allowing the function to modify the caller's dataTop-level const on a function parameter (passed by value) is ignored by the caller and mainly documents intent inside the function body