C++ Part I · Module 8 · Pointers and References
const Correctness with Pointers and References
The word const has been doing load-bearing work since Chapter 3.9 and has not had a chapter of its own since. Chapter 6.5 built the most common parameter form in C++ on it and measured all three of its properties; Chapter 8.6's table of recommended signatures used it in every row that only reads. Until now there has been one place to put it and one thing it could mean. Pointers change that, because a pointer involves two objects — the pointer itself and the thing it points at — and either can be made unmodifiable independently. That gives three spellings rather than one, and two of them differ only by which side of the asterisk the word sits on, with no visible logic connecting the position to the meaning unless you know the reading rule. Getting them the wrong way round is not a typo; it is a function promising something it does not deliver, or refusing something it should allow. The chapter gives the rule, measures all three forms against both assignments they could forbid, and then moves to the part that matters more in practice: const is only useful if it goes all the way through an interface, and a single link that drops it undoes every promise upstream of it.
Intermediate17 min readC++Part IFundamentalsReferences
Part I · Module 8 · Chapter 8.7 · const Correctness with Pointers and References
1. Two Things That Can Be const
Chapter 3.9 established const as a compile-time promise that an object will not be modified. With one object there is one promise to make.
A pointer involves two:
int* p = &x;The object p points at — may it be modified through p?
p itself — may p be made to point somewhere else?
Those are independent, so there are four combinations and three of them need a spelling:
| Declaration | May modify *p? | May reseat p? |
|---|---|---|
int* p | yes | yes |
const int* p | no | yes |
int* const p | yes | no |
const int* const p | no | no |
A note before the rule: this is const, not constexpr. Chapter 3.9 §6 drew that line — const means "will not be modified", constexpr means "computable at compile time" — and Chapter 21.2 is the latter's chapter. Nothing here involves it.
2. The Reading Rule
Read a declaration right to left, starting at the name.
That is the whole rule, and it makes all three forms fall out:
const int* p; // p is a pointer to an int that is const
int* const p; // p is a const pointer to an int
const int* const p; // p is a const pointer to an int that is constReading right to left from p:
| Declaration | Right to left | Reads as |
|---|---|---|
const int* p | p → * → int → const | "p is a pointer to a const int" |
int* const p | p → const → * → int | "p is a const pointer to an int" |
const int* const p | p → const → * → int → const | "p is a const pointer to a const int" |
Whichever side of the * the const is on tells you what it applies to. Before the *, it applies to the pointed-to type. After the *, it applies to the pointer.
One wrinkle worth knowing because you will see it: const int* and int const* mean the same thing — std::is_same<const int*, int const*>::value is measured true. Both put const before the *, so both make the pointed-to int const. The second spelling is more consistent with the right-to-left rule and some codebases prefer it for that reason; this curriculum writes const int* because it is far more common. They are interchangeable.
3. Measured: Each Form Forbids Exactly One Thing
The rule is only worth having if it predicts behaviour. Here all three forms are given the same two statements — modify the object, and reseat the pointer — and the compiler says which is rejected.
int main()
{
int a = 1, b = 2;
/* DECLARATION */ = &a;
*p = 9; // modify the pointed-to object
p = &b; // repoint
(void)b;
return 0;
}const int* p:
line 5: error: read-only variable is not assignable*p = 9 rejected. p = &b accepted.
int* const p:
line 6: error: cannot assign to variable 'p' with const-qualified type 'int *const'*p = 9 accepted. p = &b rejected.
const int* const p:
line 5: error: read-only variable is not assignable
line 6: error: cannot assign to variable 'p' with const-qualified type 'const int *const'Both rejected — two errors from one declaration.
That third result is the proof that the two consts are independent rather than two intensities of one thing. Each contributes one refusal, and together they contribute both.
Notice also that the two diagnostics are worded differently, which is a useful tell when you meet one in isolation:
- "read-only variable is not assignable" → you tried to modify the pointed-to object; the
constis before the*. - "cannot assign to variable 'p' with const-qualified type" → you tried to reseat the pointer; the
constis after the*, and the message quotes the type back at you.
4. References Need Only One Form
For a reference the question of reseating does not exist. Chapter 8.5 §4 measured that exhaustively — there is no syntax for rebinding, so there is nothing for a const after the ampersand to forbid.
const int& r = a; // cannot modify a through r
int& const r = a; // rejected outrightThe second is not merely pointless — it is an error:
error: 'const' qualifier may not be applied to a referenceThe language does not allow the question to be asked, because there is nothing for the answer to constrain.
So references have one useful form, const T&, and it is Chapter 6.5's. That chapter measured both of its halves:
The write is a compile error:
error: cannot assign to variable 'n' with const-qualified type 'const int &'And it binds to things a non-const reference refuses — a variable, a const object, and a literal, with three measured addresses in Chapter 6.5 §5.
A reference is therefore always "const in the pointer sense" and optionally const in the object sense. That asymmetry is one more instance of Chapter 8.6's point: a reference represents fewer states, so there is less to say about it.
One related form worth recognising, because it looks like it should matter and does not:
void f(int* const p); // the parameter cannot be reseated inside fThat is a promise f makes to itself, not to its caller. The caller passed a copy of a pointer — Chapter 6.2's rule — so whether f reseats its own copy is invisible outside. It is legal, it is occasionally used as a discipline, and it is not part of the interface. const on the pointed-to type is part of the interface; const on a by-value parameter is not.
5. Propagating const Through an Interface
The forms are mechanical. This is the part that decides whether const is worth anything.
A const promise is only as strong as the weakest link in the chain of calls that touch the object. Consider a function that takes a block to read:
double mean(const SampleBlock& block); // promises not to modifyIf mean passes block on to something else, that something else must also promise:
static int sum_of(const SampleBlock& block); // also const -- fine
double mean(const SampleBlock& block)
{
return sum_of(block) / 8.0; // works
}And if it does not, the call is rejected:
static int sum_of(SampleBlock& block); // NOT const
double mean(const SampleBlock& block)
{
return sum_of(block) / 8.0; // ERROR
}Measured, both directions. With sum_of taking const SampleBlock&: zero diagnostics, and mean returned 4.5 for a block holding 1 through 8. With sum_of taking SampleBlock&:
error: no matching function for call to 'sum_of'
note: candidate function not viable: 1st argument ('const SampleBlock') would lose const qualifierThe compiler will not let a const promise be laundered through a non-const parameter. Chapter 6.4 §5 measured the same refusal for an int.
constpropagates up the call chain, and it is enforced at every step. A function that promises not to modify something cannot hand it to a function that might.
The practical consequence is directional. Adding const to a leaf function is free. Adding it to a function near the top of a call chain forces every function below it to be const too, which is why retrofitting const onto an existing codebase is a cascade rather than an edit. It is also why the recommendation is to write it from the start: Chapter 6.9 §5's rule was "choose the form that makes the true promise", and doing so at every level means the cascade never has to happen.
Chapter 11.7 is where this reaches classes, and the question becomes which member functions may be called on a const object — which is the same propagation problem one level up.
6. Where const Is Permitted to Be Dropped
Two honest limits, because a chapter that only sells const would be misleading.
const_cast exists. Chapter 4.10 §6 named it and deferred it, and this is as far as this curriculum goes: it removes const from a type, it is the tool for interfacing with code that is not const-correct, and removing const in order to modify an object that was genuinely declared const is undefined behaviour. Casting away const from a pointer to a non-const object — a const T* that was handed a T — is legal; doing it to something actually const is not. If you find yourself needing it, the usual correct fix is to make the other interface const-correct.
const is a promise about one access path, not about the object. This is the subtler limit:
int x = 1;
const int& r = x; // cannot modify x through r
x = 2; // but x itself is not const -- this is fineMeasured — zero warnings, and the output is x = 2 r = 2.
r promises that name will not be used to modify. It does not make x immutable, and nothing stops the program changing x by another route — r simply reports the new value. Chapter 6.5 §4's enforcement is real and its scope is exactly one access path.
That matters most for the case Chapter 8.6 §6 raised: a const T& parameter guarantees this function will not modify the argument. It does not guarantee the argument will not change during the call — if something else holds a non-const route to the same object, it can. The guarantee is about the function's own behaviour, which is the useful thing for a reader, and it is not a guarantee of stability.
7. What to Actually Do
Read right to left from the name. It makes all three forms obvious and never fails.
const before the * protects the object; after the * protects the pointer.
Default to const T& for anything you only read — Chapter 6.5 §6's rule, unchanged.
Use const T* when you need a pointer and only read — the pointer equivalent, and the right form for the optional-input case of Chapter 8.6 §5.
Write const from the start, so you never have to run §5's cascade.
Do not write const on a by-value parameter in a declaration expecting it to mean something to callers — §4.
Do not reach for const_cast. Fix the interface that is not const-correct.
Remember the scope of the promise. const constrains one access path, not the object.
8. Mistakes Worth Making Once
Reading const int* p as an unreseatable pointer. It is the opposite — measured: p = &b is accepted and *p = 9 is not.
Reading int* const p as a pointer to a constant. Also the opposite.
Thinking the two consts are two intensities of one thing. Measured: const int* const p produces two separate errors.
Writing int& const r. Meaningless — a reference cannot be reseated anyway.
Expecting const on a by-value parameter to mean something to the caller. It does not; the caller passed a copy.
Trying to pass a const object to a non-const parameter. would lose const qualifier — Chapter 6.4 §5.
Adding const high in a call chain and expecting one edit. It cascades downward, and the cascade is the point.
Using const_cast to modify a genuinely const object. Undefined behaviour.
Reading const T& as a guarantee the object will not change. It guarantees this function will not change it.
9. Interview Relevance
"What is the difference between const int* p and int* const p?" This is one of the most frequently asked C++ questions and the expected answer is the reading rule plus which assignment each forbids. The strong version adds the third form and the fact that it produces two errors, because that shows the two consts are independent rather than recalled as a list.
"What is const correctness?" Not "using const a lot". It is that const propagates and is enforced at every step, so a function promising not to modify something cannot pass it to one that might. Mentioning that retrofitting it cascades — and that the cascade is the check finding untrue promises — is the answer of someone who has done it.
"Does const T& mean the object cannot change?" No — it means this function will not change it through that name. The distinction is §6's and it is a good discriminator.
10. What You Should Be Able to Say Now
- A pointer involves two independently const-able things: the pointed-to object, and which object is pointed at.
- Read right to left from the name.
constbefore the*applies to the object; after the*, to the pointer. const int*andint const*are the same thing.- Measured —
const int* p:*p = 9iserror: read-only variable is not assignable;p = &bis fine. - Measured —
int* const p:p = &biserror: cannot assign to variable 'p' with const-qualified type 'int *const';*p = 9is fine. - Measured —
const int* const p: both are errors, which proves the twoconsts are independent. - The two diagnostics are worded differently, and the wording tells you which
constyou hit. - References need only one form. There is no reseating to forbid — Chapter 8.5 §4.
const T&'s write is a compile error and it binds to literals andconstobjects — Chapter 6.5 §§4–5.conston a by-value parameter is a promise to yourself, not part of the interface.constpropagates up the call chain and is enforced at every step — aconstpromise cannot be handed to a non-constparameter.- So writing it from the start is free and retrofitting it cascades — and the cascade is the check working.
const_castexists; modifying a genuinelyconstobject through one is undefined behaviour.constconstrains one access path, not the object —const int& r = x;does not stopx = 2;.
11. Exercises
Exercise 1 — read them aloud. Say what each declares, right to left: (a) const int* p (b) int* const p (c) const int* const p (d) int const* p (e) const int& r.
Exercise 2 — predict the error. For each declaration initialized to &a, say which of *p = 9; and p = &b; is rejected and give the compiler's wording: (a) int* p (b) const int* p (c) int* const p (d) const int* const p.
Exercise 3 — count the errors. Explain why const int* const p produces two diagnostics rather than one, and what that tells you about the two consts.
Exercise 4 — fix the chain. double mean(const SampleBlock&) calls int sum_of(SampleBlock&) and fails. Give the error, the fix, and say which direction the change propagates.
Exercise 5 — which form? Give the parameter type for each: (a) read a large object (b) read a large object that may be absent (c) modify a large object (d) read an int (e) a pointer the function must not reseat internally.
Exercise 6 — the scope of the promise. int x = 1; const int& r = x; x = 2; compiles. Explain what r's const does and does not promise, and what that means for a const T& parameter during a call.
Exercise 7 — the cascade. You add const to one parameter near the top of a five-deep call chain and get eleven errors. Say what that means about the code before your change, and why this is a good outcome.
12. Summary
- A pointer has two independently const-able things: the object, and which object.
- Read right to left from the name; before the
*is the object, after the*is the pointer. const int*andint const*are identical.- Measured:
const int* pforbids*p = 9;int* const pforbidsp = &b;const int* const pforbids both, with two separate errors. - The diagnostic wording identifies which
constyou hit. - References need one form — there is no reseating to forbid.
conston a by-value parameter is not part of the interface.constpropagates and is enforced at every step, so writing it early is free and retrofitting cascades.const_caston a genuinelyconstobject is undefined behaviour.constconstrains an access path, not an object.
Seven chapters have now built the two sentences the module opened with into a complete working vocabulary — addresses, the two operators, the deliberate nothing, arithmetic, the alias, the choice between them, and the promises. Every one of those chapters has deferred the same thing, and three of them deferred it by name. Chapter 8.1's callout said a pointer's value refers to something whose existence it does not control. Chapter 8.3 §1 drew a table with two failure modes and took only the testable one. Chapter 8.5 §5 admitted that a reference cannot be null and can still dangle, and Chapter 8.6 §6 refused to call references safe for exactly that reason. The last chapter is the one all of them were pointing at, and the measurements are unusually complete: four distinct ways to produce it, and a table showing that the compiler catches precisely the one case the sanitizer misses while the sanitizer catches the three the compiler does not.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
