Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

DeclarationMay modify *p?May reseat p?
int* pyesyes
const int* pnoyes
int* const pyesno
const int* const pnono

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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 const

Reading right to left from p:

DeclarationRight to leftReads as
const int* pp → * → int → const"p is a pointer to a const int"
int* const pp → const → * → int"p is a const pointer to an int"
const int* const pp → 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.

Azvya Education Pvt. Ltd.VLSI Mentor
the test, run three times with the declaration changed
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line 5: error: read-only variable is not assignable

*p = 9 rejected. p = &b accepted.

int* const p:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
line 6: error: cannot assign to variable 'p' with const-qualified type 'int *const'

*p = 9 accepted. p = &b rejected.

const int* const p:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 const is before the *.
  • "cannot assign to variable 'p' with const-qualified type" → you tried to reseat the pointer; the const is 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    const int& r = a;      // cannot modify a through r
    int& const r = a;      // rejected outright

The second is not merely pointless — it is an error:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: 'const' qualifier may not be applied to a reference

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void f(int* const p);      // the parameter cannot be reseated inside f

That 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    double mean(const SampleBlock& block);        // promises not to modify

If mean passes block on to something else, that something else must also promise:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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&:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: no matching function for call to 'sum_of'
note: candidate function not viable: 1st argument ('const SampleBlock') would lose const qualifier

The 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.

const propagates 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int x = 1;
    const int& r = x;      // cannot modify x through r
    x = 2;                 // but x itself is not const -- this is fine

Measured — 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

  1. A pointer involves two independently const-able things: the pointed-to object, and which object is pointed at.
  2. Read right to left from the name. const before the * applies to the object; after the *, to the pointer.
  3. const int* and int const* are the same thing.
  4. Measured — const int* p: *p = 9 is error: read-only variable is not assignable; p = &b is fine.
  5. Measured — int* const p: p = &b is error: cannot assign to variable 'p' with const-qualified type 'int *const'; *p = 9 is fine.
  6. Measured — const int* const p: both are errors, which proves the two consts are independent.
  7. The two diagnostics are worded differently, and the wording tells you which const you hit.
  8. References need only one form. There is no reseating to forbid — Chapter 8.5 §4.
  9. const T&'s write is a compile error and it binds to literals and const objects — Chapter 6.5 §§4–5.
  10. const on a by-value parameter is a promise to yourself, not part of the interface.
  11. const propagates up the call chain and is enforced at every step — a const promise cannot be handed to a non-const parameter.
  12. So writing it from the start is free and retrofitting it cascades — and the cascade is the check working.
  13. const_cast exists; modifying a genuinely const object through one is undefined behaviour.
  14. const constrains one access path, not the object — const int& r = x; does not stop x = 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* and int const* are identical.
  • Measured: const int* p forbids *p = 9; int* const p forbids p = &b; const int* const p forbids both, with two separate errors.
  • The diagnostic wording identifies which const you hit.
  • References need one form — there is no reseating to forbid.
  • const on a by-value parameter is not part of the interface.
  • const propagates and is enforced at every step, so writing it early is free and retrofitting cascades.
  • const_cast on a genuinely const object is undefined behaviour.
  • const constrains 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.