Skip to content
C++
Language
since C++11
Basic

Master const and constexpr: From Runtime Safety to Compile-Time Power

Learn to use const for immutability guarantees and constexpr to move computation to compile time, eliminating runtime overhead.

By the end of this page, you will understand the difference between const and constexpr, know when to reach for each one, and be able to write functions and variables that the compiler evaluates at compile time β€” saving runtime cycles and catching errors earlier.

What and Why

C++ gives you two related but distinct tools for expressing that something should not change:

const means "this value cannot be modified after initialization." The compiler enforces this at the point of use β€” any attempt to write through a const binding is a compile error. The value itself, however, may not be known until the program runs.

constexpr means "this value must be computable at compile time." It implies const, but goes further: the compiler is required to evaluate it before the program ever starts. If evaluation is impossible at compile time (because it depends on runtime input), the compiler rejects it outright.

The mental model: const is a promise to the reader and the compiler that a value won't change. constexpr is a promise that the compiler can compute the value itself, right now, during compilation.

Why does this matter? Compile-time constants:

  • Eliminate magic numbers without adding runtime overhead.
  • Enable array sizes, template arguments, and if constexpr branches that are impossible with runtime values.
  • Catch logic errors earlier β€” at compile time rather than in production.

Step by Step

const β€” preventing modification

The simplest use is marking a local variable read-only:

cpp
#include <iostream>

int main() {
    const int max_retries = 3;
    // max_retries = 5; // error: assignment of read-only variable
    std::cout << max_retries << '\n';
}

const works on pointers too, and placement matters:

cpp
int value = 42;

const int* p1 = &value;       // pointer to const int β€” *p1 is read-only
int* const p2 = &value;       // const pointer to int β€” p2 cannot be reseated
const int* const p3 = &value; // both locked

Read pointer declarations right-to-left: int* const p2 is "p2 is a const pointer to int."

const on member functions

A member function marked const promises not to modify the object. This lets you call it on const instances:

cpp
#include <iostream>
#include <string>

class Config {
    std::string host_;
    int port_;
public:
    Config(std::string h, int p) : host_(std::move(h)), port_(p) {}

    const std::string& host() const { return host_; }
    int port() const { return port_; }

    void set_port(int p) { port_ = p; } // non-const: modifies state
};

void print(const Config& cfg) {
    // cfg.set_port(80); // error: cfg is const
    std::cout << cfg.host() << ':' << cfg.port() << '\n'; // fine
}

int main() {
    Config c{"example.com", 443};
    print(c);
}

Mark every getter const. This unlocks your class for use in const contexts β€” const references, const containers, and thread-safe read paths.

constexpr variables β€” compile-time constants

Requires C++11.

cpp
#include <cstddef>

constexpr double pi = 3.14159265358979;
constexpr std::size_t buffer_size = 1024;

char buf[buffer_size]; // array size known at compile time β€” valid

Compare with plain const:

cpp
#include <iostream>

int main() {
    int n;
    std::cin >> n;

    const int x = n;       // fine: x is const but runtime-valued
    // char arr[x];        // error: not a constant expression
    // constexpr int y = n; // error: n isn't known at compile time
}

constexpr functions β€” computation at compile time

A constexpr function is evaluated at compile time when all its arguments are constant expressions. Otherwise it falls back to a normal runtime call β€” you write it once and get both behaviors for free.

cpp
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

constexpr int f6 = factorial(6); // computed at compile time: 720

int main() {
    int runtime_n = 5;
    int f5 = factorial(runtime_n); // computed at runtime β€” also valid
}

constexpr in C++14 and later

C++11 constexpr functions were limited to a single return statement. C++14 relaxed this: you can use local variables, loops, and conditionals:

cpp
// C++14
constexpr int sum_to(int n) {
    int total = 0;
    for (int i = 1; i <= n; ++i)
        total += i;
    return total;
}

constexpr int s100 = sum_to(100); // 5050, computed at compile time

C++17 added if constexpr for template branching, and C++20 expanded constexpr to cover std::vector, std::string, and most of the standard library.

Common Patterns

Pattern 1: Named compile-time constants

Replace raw literals and #define with typed, scoped constexpr values:

cpp
namespace physics {
    constexpr double speed_of_light = 2.998e8; // m/s
    constexpr double planck          = 6.626e-34; // JΒ·s
}

constexpr double photon_energy(double frequency) {
    return physics::planck * frequency;
}

constexpr double visible_photon = photon_energy(6.0e14); // compile-time

Unlike #define, these are type-safe, respect namespaces, and show up in the debugger.

Pattern 2: constexpr for array sizes and template arguments

Template parameters and stack array sizes must be constant expressions. constexpr bridges readable names and these hard requirements:

cpp
constexpr int kCacheLineBytes = 64;
constexpr int kIntSlots       = kCacheLineBytes / static_cast<int>(sizeof(int));

int aligned_buf[kIntSlots]; // valid: kIntSlots is constexpr

template<int N>
struct RingBuffer { int data[N]; };

RingBuffer<kIntSlots> ring; // valid: kIntSlots is a constant expression

Pattern 3: const references as cheap, safe parameters

Pass large objects by const reference to skip the copy without risking mutation:

cpp
#include <numeric>
#include <vector>

double mean(const std::vector<double>& v) {
    if (v.empty()) return 0.0;
    return std::accumulate(v.begin(), v.end(), 0.0)
           / static_cast<double>(v.size());
}

This is idiomatic C++ for any non-trivial type you read but do not modify.

What Can Go Wrong

Mistake 1: Calling a non-const member on a const object

cpp
class Counter {
    int n_ = 0;
public:
    void increment() { ++n_; }
    int value() { return n_; } // forgot const!
};

void report(const Counter& c) {
    // c.value(); // error: 'this' loses const qualifier
}

Fix: mark value() as const:

cpp
int value() const { return n_; }

Mistake 2: Expecting const to mean compile-time

cpp
#include <iostream>

int read_port() {
    int p; std::cin >> p; return p;
}

int main() {
    const int port = read_port(); // fine: const but runtime-valued
    // template<int N> struct S {};
    // S<port> s; // error: port is not a constant expression
}

Fix: if you need a compile-time constant, use constexpr. If the value isn't known until runtime, you cannot use it as a template argument or array size β€” that's a fundamental constraint, not a compiler quirk.

Mistake 3: A constexpr function that cannot evaluate at compile time

cpp
#include <cstdlib>

constexpr int random_val() {
    return std::rand(); // error: std::rand is not constexpr
}

Fix: constexpr functions may only call other constexpr functions when used in a constant expression. Either drop constexpr or restructure the logic to use only compile-time-safe operations.

Mistake 4: Pointer const placement confusion

cpp
int a = 1, b = 2;
const int* p = &a;
p = &b;    // fine: you can reseat the pointer
// *p = 3; // error: you cannot modify the value through p

The star divides the declaration: const to the left protects what is pointed to; const to the right protects the pointer itself. When in doubt, read the declaration right-to-left.

Quick Reference

Featureconstconstexpr
Value immutableYesYes (implied)
Compile-time evaluation requiredNoYes (in constant-expression contexts)
May depend on runtime inputYesNo
Works on member functionsYesYes
Works on free functionsNoYes
Enables template arguments / array sizesNoYes
Safe replacement for #definePartialFull (for values)
Minimum standardC++98C++11
Loops and locals in function bodyN/AC++14+

Rules of thumb:

  • Use const when you want immutability but the value may come from runtime.
  • Use constexpr when you want β€” or require β€” compile-time evaluation.
  • Use const& for function parameters to avoid copying large objects.
  • Mark every getter const.

What's Next

With const and constexpr solid, the natural next steps are:

  • Const Correctness β€” a deeper look at propagating const through an entire class interface and avoiding the const_cast trap.
  • if constexpr β€” use compile-time branching inside templates to select code paths cleanly, without SFINAE.
  • Advanced constexpr β€” consteval, constinit, and compile-time containers introduced in C++20.