const & constexpr Basics
Reviewed & published by Brayan K
Some values should never change: a tax rate, the number of squares on a chessboard, pi. Marking them const tells the compiler to reject any accidental reassignment, and constexpr goes further by computing values at compile time. "Const-correctness" — using const everywhere you can — is one of the habits that separates solid C++ from buggy C++. This lesson makes it second nature.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Lock a value as read-only with const
- Use constexpr for true compile-time constants
- Size arrays and array<T,N> with constexpr values
- Pass large objects by const reference to avoid copies
- Write const member functions for read-only methods
- Define constexpr functions that work at compile and run time
💡 Real-World Analogy
Think of const as writing a value in permanent ink instead of pencil — you can read it forever, but you can't erase and rewrite it. constexpr is stronger still: it's like a value engraved into the building's cornerstone before the doors even open — fixed at construction time (compile time), so it can be used as part of the structure itself, such as deciding how many rooms (array slots) to build. Both protect you: if you (or a teammate) ever try to change something that was meant to be fixed, the compiler stops you immediately instead of letting a subtle bug slip through.
📊 const vs constexpr
| Aspect | const | constexpr |
|---|---|---|
| Can be reassigned? | no | no |
| Known when? | run time (or compile) | compile time (guaranteed) |
| Can size an array? | only if also compile-time | yes |
| Initializer | any expression | must be a constant expression |
| Typical use | read-only values, params | true compile-time constants |
Rule of thumb: use const by default for anything you don't intend to change; promote to constexpr when the value is a fixed compile-time constant.
1. const: Read-Only Values
Add const before a type and the variable becomes read-only: you must initialize it on the spot, and any later attempt to reassign it is a compile error. This is a feature, not a restriction — it documents intent ("this never changes") and lets the compiler catch accidental writes. By convention, constants are named in UPPER_SNAKE_CASE.
#include <iostream>
using namespace std;
int main() {
// const = read-only once set. The compiler blocks any reassignment.
const double PI = 3.14159;
const int MAX_USERS = 100;
cout << "PI = " << PI << endl; // PI = 3.14159
cout << "max users = " << MAX_USERS << endl; // max users = 100
// PI = 3.14; // ❌ error: assignment of read-only variable 'PI'
// MAX_USERS = 200; // ❌ error: assignment of read-only variable
// A const can still be USED in calculations -- just not changed:
double area = PI * 5 * 5;
cout << "area = " << area << endl; // area = 78.5398
return 0;
}
// ✅ Expected output:
// PI = 3.14159
// max users = 100
// area = 78.53972. constexpr: Compile-Time Constants
constexpr is a promise that a value can be computed at compile time. That unlocks uses where C++ requires a compile-time constant — most notably the size of a fixed array or a std::array<T, N>. Every constexpr is implicitly const, but it adds the stronger guarantee of compile-time evaluation.
#include <iostream>
#include <array>
using namespace std;
int main() {
// constexpr = a value known at COMPILE time (stronger than const).
constexpr int SIZE = 10;
constexpr double GRAVITY = 9.81;
// Because SIZE is a compile-time constant, it can size an array:
int data[SIZE]; // OK: SIZE is constexpr
array<int, SIZE> grid{}; // OK too
for (int i = 0; i < SIZE; ++i) data[i] = i * i;
cout << "data[9] = " << data[9] << endl; // data[9] = 81
cout << "grid size = " << grid.size() << endl; // grid size = 10
cout << "gravity = " << GRAVITY << endl; // gravity = 9.81
return 0;
}
// ✅ Expected output:
// data[9] = 81
// grid size = 10
// gravity = 9.81Your turn. Choose the right keyword for each constant — one needs to size an array (so it must be compile-time):
#include <iostream>
using namespace std;
int main() {
// 🎯 YOUR TURN — declare two constants the right way.
// 1) A run-time-safe read-only tax rate of 0.2:
___ double TAX_RATE = 0.2; // 👉 const
// 2) A compile-time constant board size of 8 (used to size an array):
___ int BOARD = 8; // 👉 constexpr
int squares[BOARD]; // needs BOARD to be compile-time
double price = 50.0;
cout << "tax = " << price * TAX_RATE << endl; // ✅ tax = 10
cout << "cells = " << BOARD * BOARD << endl; // ✅ cells = 64
return 0;
}🧩 Reorder Challenge
These lines use a constexpr size to build and fill an array. Put them in order:
A. int data[N];
B. constexpr int N = 4;
C. int main() {
D. for (int i = 0; i < N; ++i) data[i] = i;
E. cout << data[3] << endl;
F. return 0;
G. }Correct order: C → B → A → D → E → F → G
Open main (C), declare the constexpr size first (B), use it to size the array (A), fill it (D), print data[3] = 3 (E), then return 0; (F) and close (G). N must be a compile-time constant declared before it sizes the array.
3. const References in Parameters
A huge practical use of const is in function parameters. Passing a large object by value copies it; passing by const& hands over a reference (no copy) while promising the function won't modify it. This pattern — const std::string&, const std::vector<T>& — is everywhere in good C++ code: fast and safe.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// Pass big objects by CONST REFERENCE: no copy, and read-only.
double averageLength(const vector<string>& words) {
if (words.empty()) return 0.0;
int total = 0;
for (const string& w : words) total += w.size();
return static_cast<double>(total) / words.size();
}
int main() {
vector<string> names = {"Ann", "Beatrice", "Cy"};
// 'names' is NOT copied here -- it's passed by const reference.
cout << "avg length = " << averageLength(names) << endl; // avg length = 4.66667
return 0;
}
// ✅ Expected output:
// avg length = 4.333334. const Member Functions
A member function with const after its parameter list promises not to modify the object it's called on. Getters are the classic example. The payoff: a const object may only call const member functions, so marking your read-only methods const lets them work on constants and documents that they don't mutate state.
#include <iostream>
#include <string>
using namespace std;
struct BankAccount {
string owner;
double balance;
// const member function: promises NOT to modify the object.
double getBalance() const { return balance; } // read-only
// non-const: it CHANGES the object, so it can't be const.
void deposit(double amount) { balance += amount; }
};
int main() {
BankAccount acc{"Mia", 100.0};
acc.deposit(50.0);
cout << "balance: " << acc.getBalance() << endl; // balance: 150
// A const object can ONLY call const member functions:
const BankAccount frozen{"Leo", 500.0};
cout << "frozen: " << frozen.getBalance() << endl; // frozen: 500
// frozen.deposit(10); // ❌ error: can't modify a const object
return 0;
}
// ✅ Expected output:
// balance: 150
// frozen: 500🔎 Deep Dive: constexpr functions
A constexpr function is one the compiler can evaluate at compile time when its arguments are themselves constant. The same function still works at run time when given runtime values — you write it once and get both:
#include <iostream>
using namespace std;
// A constexpr FUNCTION can run at compile time OR run time.
constexpr int square(int n) { return n * n; }
constexpr long long factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
int main() {
// Evaluated at COMPILE time (the result is baked into the binary):
constexpr int nine = square(3);
constexpr long long f5 = factorial(5);
int lookup[square(4)]; // size 16, computed at compile time
cout << "square(3) = " << nine << endl; // square(3) = 9
cout << "factorial(5) = " << f5 << endl; // factorial(5) = 120
cout << "array size = " << sizeof(lookup)/sizeof(int) << endl; // 16
// Same function, RUN-time argument -> still works:
int x = 7;
cout << "square(x) = " << square(x) << endl; // square(x) = 49
return 0;
}
// ✅ Expected output:
// square(3) = 9
// factorial(5) = 120
// array size = 16
// square(x) = 49Computing values at compile time means zero runtime cost and lets results be used where the language demands a constant. In C++20, consteval forces compile-time evaluation, and constinit guarantees compile-time initialization of globals.
Pro Tips
- 💡 Const by default: if you don't plan to change a value, mark it const — let the compiler guard it.
- 💡 Use const& for big parameters: strings, vectors, and structs should usually be taken by const&.
- 💡 Mark getters const: any method that doesn't change the object should say so.
- 💡 Reach for constexpr for fixed constants: buffer sizes, physics constants, lookup-table dimensions.
🧠 Quick Recall
Predict the output (or whether it compiles) before revealing the answer.
1. Does this compile?
const int LIMIT = 5;
LIMIT = 10;
cout << LIMIT;No — LIMIT is const, so LIMIT = 10; is an error: "assignment of read-only variable".
2. Does this compile?
int n = 4;
int arr[n]; // is this OK?
constexpr int m = 4;
int brr[m]; // and this?In standard C++, int arr[n]; with a non-const n is not allowed (variable-length arrays aren't standard). int brr[m]; with constexpr m is fine — it's a compile-time constant.
3. Does the last line compile?
struct C { int v; int get() const { return v; } };
const C c{7};
cout << c.get(); // ?Yes — prints 7 — get() is a const member function, so a const object may call it.
Common Errors (and the fix)
- Reassigning a const: "assignment of read-only variable" means you tried to change a const. Either don't, or drop the const if it truly needs to change.
- Forgetting to initialize a const: const int x; is an error — a const must be given its value at declaration.
- Sizing an array with a non-const: int a[n]; for a runtime n isn't standard C++. Use constexpr, or a std::vector.
- Calling a non-const method on a const object: the compiler refuses. Mark read-only methods const.
- constexpr with a runtime initializer: constexpr int x = readInput(); fails — the initializer must be a constant expression. Use const instead.
📋 Quick Reference
| Task | Code |
|---|---|
| Read-only value | const double PI = 3.14159; |
| Compile-time constant | constexpr int SIZE = 10; |
| Read big param | void f(const string& s) |
| Read-only method | int get() const { ... } |
| constexpr function | constexpr int sq(int n){...} |
| const pointer-to-const | const int* const p |
Mini-Challenge: Circle Calculator
Put a constexpr constant and const-correct functions together to compute a circle's area and circumference.
#include <iostream>
using namespace std;
// 🎯 MINI-CHALLENGE: Circle calculator with constants
// 1. Declare a constexpr double PI = 3.14159.
// 2. Write a const-correct function:
// double area(double radius) -> PI * radius * radius
// Pass radius however you like (double is cheap to copy).
// 3. Make a const double r = 5.0 in main and print the area.
// 4. BONUS: write circumference(double r) -> 2 * PI * r and print it.
//
// ✅ Example output:
// area = 78.5398
// circumference = 31.4159
int main() {
// your code here
return 0;
}🎉 Lesson Complete
- ✅ const makes a value read-only; reassigning it is a compile error
- ✅ constexpr guarantees a compile-time constant — it can size arrays
- ✅ Pass large objects by const& for speed without copies
- ✅ const member functions promise not to mutate the object and work on const objects
- ✅ constexpr functions run at compile time or run time, written once
Practice quiz
What does 'const' do to a variable?
- Makes it global
- Forces compile-time evaluation
- Marks it read-only so it cannot be reassigned after initialization
- Makes it faster
Answer: Marks it read-only so it cannot be reassigned after initialization. const is read-only once set; any attempt to reassign it is a compile error.
What is the key difference between const and constexpr?
- constexpr requires a value computable at compile time; const may be computed at run time
- No difference
- const is compile-time, constexpr is run-time
- constexpr can be reassigned
Answer: constexpr requires a value computable at compile time; const may be computed at run time. Both can't be reassigned, but constexpr guarantees compile-time evaluation; const only guarantees read-only.
Which can be used as the size of a fixed-size array?
- Any const
- Any runtime int
- Only literals
- A constexpr value (or a const that is itself a compile-time constant)
Answer: A constexpr value (or a const that is itself a compile-time constant). Array sizes require a compile-time constant; constexpr guarantees this.
Why pass a large object by const reference (const T&)?
- To copy it safely
- To avoid copying while promising not to modify it
- To make it mutable
- To force compile-time evaluation
Answer: To avoid copying while promising not to modify it. const& gives the speed of passing by reference (no copy) and the safety of read-only access.
What does marking a member function 'const' (after its parameter list) promise?
- It will not modify the object it is called on
- It returns a const value
- It is evaluated at compile time
- It is private
Answer: It will not modify the object it is called on. A const member function promises not to mutate the object, so const objects may call it.
A const object can call which member functions?
- Any member function
- Only static functions
- Only const member functions
- None
Answer: Only const member functions. A const object may only call const member functions, which is why getters should be marked const.
Does 'const int LIMIT = 5; LIMIT = 10;' compile?
- Yes
- No — 'assignment of read-only variable'
- Only with -std=c++20
- Only if LIMIT is global
Answer: No — 'assignment of read-only variable'. Reassigning a const is a compile error.
Can a single constexpr function be called at run time too?
- No, never
- Only if marked inline
- Only with consteval
- Yes — it runs at compile time when its arguments are constant, and at run time otherwise
Answer: Yes — it runs at compile time when its arguments are constant, and at run time otherwise. A constexpr function works in both modes depending on whether its arguments are compile-time constants.
Why does 'constexpr int x = readInput();' fail to compile?
- readInput is private
- The initializer must be a constant expression, but a runtime value isn't known at compile time
- constexpr needs a double
- x must be const
Answer: The initializer must be a constant expression, but a runtime value isn't known at compile time. constexpr demands a compile-time-known initializer; use plain const for a runtime value.
Every constexpr variable is also:
- static
- inline
- const
- volatile
Answer: const. constexpr implies const and adds the stronger guarantee of compile-time evaluation.
Continue this course
- Previous: Inheritance & Polymorphism
- Next: Checkpoint: Data & Memory — Combine vectors, maps, lambdas and smart pointers — then a quiz
- Quick reference: C++ cheat sheet
Frequently asked questions
What does const mean in C++?
const marks something as read-only: once it is set, you cannot reassign it. The compiler enforces this, so any attempt to modify a const value is a compile error. It is used for values, function parameters, references, and member functions.
What is the difference between const and constexpr?
const means the value cannot change after initialization, but it may be computed at run time. constexpr is stronger: the value must be computable at compile time, so the compiler can bake it in and use it where a compile-time constant is required, such as an array size.
Why should I pass parameters by const reference?
Passing a large object by const reference (const T&) avoids copying it while promising the function will not modify it. You get the speed of passing by reference and the safety of read-only access.
What is a const member function?
A member function declared const after its parameter list promises not to modify the object it is called on. This lets you call it on const objects and clearly documents that it is a read-only operation, like a getter.
Can a constexpr function be called at run time too?
Yes. A constexpr function runs at compile time when its arguments are constant expressions, and at run time otherwise. So you can use the same function for both compile-time constants and ordinary runtime values.