This page is the drill floor for the parent topic on const . The parent told you the rules; here we grind through every case class so you never meet a const situation you haven't already seen.
Before we start, one reminder we will lean on constantly:
Intuition The one sentence to hold in your head
const is a promise not to write through this particular name . The compiler is the referee — it rejects any line that would break the promise, at compile time , for zero runtime cost. Every example below is really the same question: "which write did the referee just blow the whistle on, and why?"
Think of const as living in three worlds — plain variables, pointers, and member functions — and each world has "edge" cases. The table below enumerates every cell we must cover. Each worked example is tagged with the cell(s) it hits.
#
World
Case class
What could go wrong / be subtle
C1
variable
plain const reassign
write after init
C2
variable
uninitialized const
no value ever possible
C3
pointer
const int* p (pointer-to-const)
write *p vs repoint p
C4
pointer
int* const p (const-pointer)
repoint p vs write *p
C5
pointer
const int* const p (both)
both writes blocked
C6
pointer
pointing a const int* at a plain int
is that legal?
C7
member fn
calling non-const method on a const object
which overload matches
C8
member fn
const/non-const at() overload pair
compiler picks by object-constness
C9
member fn
mutable escape hatch
write inside a const method
C10
limiting/UB
const_cast on truly-const data
the undefined-behavior cliff
C11
shallow-const
const object with a pointer member
pointee stays writable
C12
word problem
designing a read-only API
putting it all together
We'll now walk C1–C12 in 9 examples (some examples clear two cells at once). Every example makes you forecast first.
Worked example Which two lines does the compiler reject, and why?
const int limit = 100 ; // line A
limit = 50 ; // line B
const int flag; // line C
Forecast: Guess which of B and C fail before reading on.
Step 1 — line A is fine. We initialized limit at its declaration.
Why this step? A const variable's only legal moment to receive a value is at declaration — and we used it. ✅
Step 2 — line B is rejected. limit = 50; is a write to a name we promised never to write.
Why this step? Reassignment is a write. The referee blows the whistle: assignment of read-only variable 'limit'. ❌
Step 3 — line C is rejected. const int flag; has no initializer.
Why this step? If we can't write it now, and we can never write it later, it could never get a value. So the compiler forbids the declaration outright: uninitialized const 'flag'. ❌
Verify: Two errors (B and C), one OK (A). Sanity check: a const has exactly one write opportunity — the initializer — so any code path with zero writes (C) or two writes (A+B) is illegal.
Worked example Predict OK/ERROR for each marked line.
int a = 10 , b = 20 ;
const int* p = & a; // line 1
* p = 99 ; // line 2
p = & b; // line 3
Forecast: Which of lines 2 and 3 survive?
Read the type right to left : p is a pointer to a const int . So the int is protected through p , but p itself is a free bird. Look at the figure — the lock is on the box , not on the arrow .
Worked example (continued)
Step 1 — line 1 OK, and this is the subtle cell C6. We point a const int* at a plain (non-const) int a.
Why this step? Adding a promise is always allowed. p promises "I won't write a" — even though a itself is writable by other names. You may strengthen a promise, never weaken one silently.
Step 2 — line 2 ERROR. *p = 99 writes the pointee through p.
Why this step? The const sits left of *, qualifying the pointee. The referee: assignment of read-only location '*p'. ❌
Step 3 — line 3 OK. p = &b moves the arrow.
Why this step? Nothing locks p itself; only what it points to is read-only. ✅
Verify: a is still 10 (line 2 failed). And a can still change through its own name — a = 5; compiles fine — proving the lock lived on the access path p, not on the object a.
Worked example Predict OK/ERROR.
int a = 10 , b = 20 ;
int* const q = & a; // line 1
* q = 99 ; // line 2
q = & b; // line 3
Forecast: This is the mirror image of Example 2. Which line flips?
Now const is right of *, so it locks the pointer q itself. The arrow is glued down; the box is open. See the figure: the red lock is on the arrow's tail .
Worked example (continued)
Step 1 — line 1 OK (and mandatory). A const pointer, like any const, must be initialized. int* const q; alone would fail exactly like Example 1 line C.
Why this step? Same one-write rule: the pointer value can only be set here.
Step 2 — line 2 OK. *q = 99 writes the plain int.
Why this step? The pointee is a plain int, nothing read-only about it. ✅ Now a == 99.
Step 3 — line 3 ERROR. q = &b tries to repoint the glued arrow.
Why this step? q is const. Referee: assignment of read-only variable 'q'. ❌
Verify: After the code, a == 99, b == 20, and q still points at a. Compare with Example 2's outcome (a == 10): the same three lines give opposite results — the whole difference is one word's position .
For a deeper tour of the pointer machinery itself see Pointers and References in C++ .
Worked example What can you legally do with
const int* const r = &a;?
int a = 10 , b = 20 ;
const int* const r = & a; // line 1
* r = 99 ; // line 2
r = & b; // line 3
int x = * r; // line 4
Forecast: How many of lines 2–4 compile?
Step 1 — line 1 OK. Read right-to-left: "r is a const pointer to a const int." Two promises stacked.
Why this step? Both locks present, both initialized in one shot. ✅
Step 2 — line 2 ERROR. Writing *r breaks the pointee lock (the left const). ❌
Step 3 — line 3 ERROR. Repointing r breaks the pointer lock (the right const). ❌
Step 4 — line 4 OK. int x = *r; reads through r.
Why this step? const blocks writes , never reads . Reading is always allowed. ✅ x == 10.
Verify: Exactly one of lines 2–4 compiles (line 4), and x == 10. This is the "glass display case for BOTH finger and case" from the parent's Feynman analogy.
Worked example Which call fails?
struct Circle {
double r;
double area () const { return 3.14159 * r * r; } // reads only
void setR ( double x ) { r = x; } // writes
};
const Circle c{ 2.0 }; // a const object
double A = c. area (); // call 1
c. setR ( 5.0 ); // call 2
Forecast: c is const — which call does the referee reject?
Step 1 — call 1 OK. area() is marked const, so its hidden this has type const Circle*.
Why this step? c is const, so calling it hands a const Circle* as this. area() accepts that. ✅
Step 2 — call 2 ERROR. setR is non-const; its this is Circle* (writable).
Why this step? Passing c (a const object) would require converting const Circle* → Circle*, i.e. dropping a promise silently. Forbidden. Referee: passing 'const Circle' as 'this' argument discards qualifiers. ❌
Verify: area() on r = 2.0 gives 3.14159 × 2 2 = 12.56636 . And a non -const Circle d{2.0}; would allow both calls — proving the block in call 2 comes purely from the object's const-ness. (More on the hidden this in Classes and the this pointer .)
at() runs for each object?
struct Buffer {
char data[ 100 ];
char& at ( int i ) { return data[i]; } // A: writable
const char& at ( int i ) const { return data[i]; } // B: read-only
};
Buffer nb; nb.data[ 3 ] = 'x' ;
const Buffer cb = nb;
char w = nb. at ( 3 ); // pick A or B?
char r = cb. at ( 3 ); // pick A or B?
nb. at ( 3 ) = 'Z' ; // legal?
cb. at ( 3 ) = 'Z' ; // legal?
Forecast: For a non-const Buffer, which overload? For a const one?
Step 1 — nb.at(3) picks A. nb is non-const, so overload resolution prefers the non-const method (its this type matches exactly).
Why this step? Between two candidates, the one requiring no const-conversion wins. Returns char& → writable.
Step 2 — cb.at(3) picks B. cb is const, so only the const overload's this (const Buffer*) matches at all.
Why this step? The non-const overload would need to drop cb's const — impossible — so it's not even a candidate. Returns const char&.
Step 3 — nb.at(3) = 'Z'; OK. A returns char&, a writable reference; assigning through it writes data[3]. ✅
Step 4 — cb.at(3) = 'Z'; ERROR. B returns const char&; you cannot assign through a read-only reference. ❌
Verify: After nb.at(3) = 'Z', nb.data[3] == 'Z' (ASCII 90). r read earlier held 'x' (ASCII 120). This exact pattern powers std::vector's two operator[]s — see the const operator overload note .
Worked example Does this compile? What is
hits afterward?
struct Cache {
mutable int hits = 0 ;
int value = 42 ;
int get () const { ++ hits; return value; } // writes hits inside const!
};
const Cache q{};
int v1 = q. get ();
int v2 = q. get ();
// q.hits == ?
Forecast: get() is const but writes hits. Legal?
Step 1 — it compiles. hits is mutable, so it is exempt from the const-method write ban.
Why this step? mutable says "this member is not part of the object's logical state" — a cache counter doesn't change what the object means . So const methods may still write it. See mutable and logical constness .
Step 2 — value stays untouched. get() only reads value; writing it would fail because value is a normal member of a const object.
Why this step? Only the mutable member gets the pass; everything else obeys the const promise.
Verify: Two calls → hits == 2, and both v1 == v2 == 42. The observable value never changed; only the bookkeeping counter moved. That is exactly what "logical constness" means.
Worked example Two traps in one — spot the UB.
// Trap 1: shallow const
struct Node { int* ptr; };
int owned = 7 ;
const Node n{ & owned};
// n.ptr = &owned2; // would this fail?
* n.ptr = 999 ; // does THIS fail?
// Trap 2: const_cast on truly-const data
const int frozen = 5 ;
int* danger = const_cast<int*> ( & frozen);
* danger = 6 ; // compiles — but what happens?
Forecast: In Trap 1, which write is blocked? In Trap 2, is *danger = 6 well-defined?
Step 1 — Trap 1: n.ptr = ... is blocked, *n.ptr = 999 is allowed.
Why this step? const is shallow (bitwise) . n being const makes the member ptr a const pointer (can't repoint) — but the int it points to is a separate object, untouched by n's const. So *n.ptr = 999 legally sets owned to 999. The lock reached the arrow, not the far box.
Step 2 — Trap 2 compiles but is Undefined Behavior. frozen was originally declared const. Casting away that const and writing is UB — the compiler may have folded frozen into read-only memory or into an immediate 5 everywhere.
Why this step? const_cast only removes the promise from the type ; it does not make the underlying storage writable if it truly wasn't. See Undefined Behavior in C++ .
Verify: Trap 1 outcome is defined: owned == 999. Trap 2 has no defined outcome — frozen might read back as 5 or 6 depending on optimization; that unpredictability is the bug. Rule: const_cast-to-write is only safe when the object underneath was actually non-const to begin with.
Worked example You must design a
Log class where callers can read any reading and read the average, but cannot mutate stored data through a read-only handle, and the average is cached so it isn't recomputed unless a new reading arrives. Decide which methods are const, which members are mutable, and which function parameter is const Log&.
Forecast: Before reading the solution, guess: (1) which methods should be const? (2) which two members should be mutable? (3) which parameter should be const Log&?
Here is the full, compilable class and driver. Every name it introduces is used in the steps that follow — temps is the array of stored readings, n is how many are stored, cachedAvg is the last computed average, and dirty is a flag that is true when cachedAvg is stale.
Worked example (solution — the complete class)
#include <cstddef>
class Log {
double temps[ 3 ]; // storage for up to 3 readings (private)
std :: size_t n = 0 ; // how many readings are currently stored (0..3)
mutable double cachedAvg = 0.0 ; // member (1): last computed average
mutable bool dirty = true ; // member (2): true => cachedAvg is stale
public:
Log () : temps { 0.0 , 0.0 , 0.0 } {}
// WRITE door: the ONLY method that mutates stored readings. Non-const on purpose.
void add ( double t ) {
if (n < 3 ) {
temps[n] = t;
++ n;
dirty = true ; // a new reading invalidates the cache
}
}
// READ: const, returns one stored reading by index.
double at ( std :: size_t i ) const { return temps[i]; }
// READ: const, how many readings are stored.
std :: size_t size () const { return n; }
// READ: const, but writes the mutable cache members.
double average () const {
if (dirty) { // recompute only when stale
double sum = 0.0 ;
for ( std :: size_t i = 0 ; i < n; ++ i) sum += temps[i];
cachedAvg = (n == 0 ) ? 0.0 : sum / static_cast<double> (n);
dirty = false ; // cache is now fresh
}
return cachedAvg;
}
};
// read-only VIEW: promises "I only read L, I won't wreck it".
double report ( const Log & L ) {
double total = 0.0 ;
for ( std :: size_t i = 0 ; i < L. size (); ++ i) total += L. at (i); // reads only
return L. average (); // must work through a const handle
}
int main () {
Log log;
log. add ( 10 ); log. add ( 20 ); log. add ( 30 ); // WRITE via the one door
double a = report (log); // READ-only view: a == 20
// report() can NOT call log.add(...) — add is non-const.
return 0 ;
}
Step 1 — make temps and n private. Storage is hidden behind the class boundary.
Why this step? The requirement says callers must not mutate stored data through a read-only handle. If temps were public, log.temps[0] = 999; would sidestep every const in the design. Encapsulation is the wall; const is the one-way valve set into that wall.
Step 2 — put the single write door add() and leave it non-const. This is the only method that changes temps/n.
Why this step? Concentrating all writes in one non-const method means a const Log& handle cannot reach any of them: L.add(...) is rejected because L's this is const Log* and add demands a writable Log* — the exact mechanism of Example 5, call 2.
Step 3 — mark the read methods at(), size(), average() as const.
Why this step? Marking read-only methods const up front is what lets report's const Log& parameter call them at all (a const handle can only call const methods, Example 5). Retrofitting later would cascade edits through every const path — the parent's §4 mistake. See Function parameters — pass by value vs const reference .
Step 4 — mark cachedAvg and dirty as mutable.
Why this step? average() is const because logically it only reports data — yet it must write cachedAvg and flip dirty to lazily fill the cache. Those two members hold bookkeeping , not logical state: which temperatures were logged is unchanged. That is precisely the mutable justification of Example 7. See mutable and logical constness .
Step 5 — trace the caching flag. After add(10); add(20); add(30) we have n == 3 and dirty == true. The first average() sees dirty, computes ( 10 + 20 + 30 ) /3 = 20 , stores cachedAvg = 20, sets dirty = false. A second average() sees dirty == false and returns the cached 20 without re-summing.
Why this step? This shows the mutable members earning their keep — the whole point of logical (not bitwise) constness.
Step 6 — make report's parameter const Log&.
Why this step? const& says "I read L, I won't wreck it", it lets callers pass const logs, and it avoids copying the object. Because L is a const handle, the compiler guarantees report never touches the write door.
Verify: With readings {10, 20, 30} and n == 3, average() gives ( 10 + 20 + 30 ) /3 = 20 , and the cached second call also returns 20. So report(log) == 20. Degenerate case: an empty Log (n == 0) returns average() == 0 via the (n == 0) ? 0.0 guard — no divide-by-zero . Every method report touches is const, and the only mutating method (add) is unreachable through the const Log& — so the API is provably read-only-safe.
Recall Cover the answers — one line each
const int* p — which line fails, *p = 9 or p = &b? ::: *p = 9 fails; p = &b is fine (pointee is locked, pointer roams).
int* const p — which write is blocked? ::: p = &b (repointing); *p = 9 is allowed.
Why must const int* const r be initialized at declaration? ::: The pointer part is const, so its value can only ever be set once — at the initializer.
A const object — which methods may you call? ::: Only const member functions (their this is const T*).
Why can mutable int hits change inside a const method? ::: It is exempt from bitwise const because it holds non-logical state (a counter/cache).
Is writing through const_cast on an originally-const object defined? ::: No — it is Undefined Behavior.
const Node n with member int* ptr — can you do *n.ptr = 9? ::: Yes; const is shallow, so the far pointee stays writable (only repointing ptr is blocked).
Mnemonic The whole page in one line
Same code, one word's position, opposite result — that word is const, and it always locks the thing on its left .