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 constexprbranches 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:
#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:
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 lockedRead 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:
#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.
#include <cstddef>
constexpr double pi = 3.14159265358979;
constexpr std::size_t buffer_size = 1024;
char buf[buffer_size]; // array size known at compile time β validCompare with plain const:
#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.
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:
// 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 timeC++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:
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-timeUnlike #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:
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 expressionPattern 3: const references as cheap, safe parameters
Pass large objects by const reference to skip the copy without risking mutation:
#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
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:
int value() const { return n_; }Mistake 2: Expecting const to mean compile-time
#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
#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
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 pThe 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
| Feature | const | constexpr |
|---|---|---|
| Value immutable | Yes | Yes (implied) |
| Compile-time evaluation required | No | Yes (in constant-expression contexts) |
| May depend on runtime input | Yes | No |
| Works on member functions | Yes | Yes |
| Works on free functions | No | Yes |
| Enables template arguments / array sizes | No | Yes |
Safe replacement for #define | Partial | Full (for values) |
| Minimum standard | C++98 | C++11 |
| Loops and locals in function body | N/A | C++14+ |
Rules of thumb:
- Use
constwhen you want immutability but the value may come from runtime. - Use
constexprwhen 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
constthrough an entire class interface and avoiding theconst_casttrap. 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.