CppConstants (const, constexpr)

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 const reference parameter (const std::string& name) is the standard way to pass large objects efficiently without allowing the function to modify the caller's data

  • Top-level const on a function parameter (passed by value) is ignored by the caller and mainly documents intent inside the function body