AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

06 Arrays, pointers and references / Lesson 5 of 6

Const correctness — reading declarations right to left

13 min · +10 XP · 2 exercises · 7 questions

Read the text13 min
Exercises0/2
Quiz7 q

Quick answer

What is the difference between const int* and int* const?

const int* p is a pointer to a const int: you cannot change the value through p, but p can point elsewhere. int* const p is a const pointer to an int: the value can change, but p cannot be reseated. Read C++ declarations right to left — const left of the * applies to the pointee, right of it to the pointer; const int* const locks both.

const is the promise "this code does not modify that object", and the compiler enforces it. Placed on a parameter it tells the caller their object is safe; placed on a member function it tells a const object which operations are allowed; placed on a pointer it says whether you may change the pointee, the pointer, or neither. This lesson settles the three pointer placements and the right-to-left reading rule, the conversions const allows and forbids, why the property spreads through a codebase, and why const_cast is a warning sign rather than a tool.

Three placements, one reading rule

int x = 1, y = 2;

const int* a = &x;         // pointer to const int
int const* b = &x;         // the same type, written the other way round
int* const c = &x;         // const pointer to int
const int* const d = &x;   // const pointer to const int

Read each declaration from the name, right to left:

DeclarationReads as*p = 5p = &y
const int* pp is a pointer to a const interrorok
int const* pthe sameerrorok
int* const pp is a const pointer to an intokerror
const int* const pp is a const pointer to a const interrorerror

The rule that generates the table: const left of the * applies to what is pointed at; const right of the * applies to the pointer itself. const int* and int const* differ only in style; const T* is what most code writes. The same reading works for references — const int& r is "r is a reference to a const int" — and there is no int& const, because a reference cannot be reseated anyway.

int* const is rare in practice: a pointer you never reseat is usually a reference. const int* is everywhere — every "look but do not touch" parameter and every begin/end pair over data you only read.

What const on the pointee actually promises

int x = 1;
const int* p = &x;
// *p = 2;               // error: assignment of read-only location
x = 2;                   // fine: x itself is not const
std::cout << *p << '\n'; // 2 — p sees the change

const int* restricts your access through p, not the object: anyone holding x, or an int* to it, may still write, and p observes the new value. const on a pointer or reference describes the view, not the thing.

Conversions: adding const is free, removing it is a cast

int x = 1;
int* p = &x;
const int* cp = p;               // fine: int* → const int* adds a restriction
// int* q = cp;                  // error: would drop the restriction silently
int* q = const_cast<int*>(cp);   // compiles, and is the smell discussed below

T* converts implicitly to const T*, T& to const T&, and a std::vector<int> argument binds to a const std::vector<int>& parameter. Going the other way needs const_cast, on purpose, in writing. This asymmetry is the design: a function that takes const T& can be called with anything, and it cannot leak a non-const handle out.

Parameters

Module 4 gave the three choices — T, T&, const T& — and const is what makes the third one honest. A const std::string& parameter costs one pointer to pass, accepts a literal (a temporary binds to it, lesson 4), and guarantees the caller's string comes back unchanged:

int count_char(const std::string& text, char c) {
    int n = 0;
    for (char ch : text) {
        if (ch == c) ++n;
    }
    return n;
}

A top-level const on a by-value parameter — void f(const int n) — protects only the function's own copy; it means nothing to callers.

const member functions — a preview

struct Counter {
    int value = 0;
    int get() const { return value; }   // promises not to modify *this
    void add(int n) { value += n; }     // may modify
};

void report(const Counter& c) {
    std::cout << c.get() << '\n';       // OK: get() is const
    // c.add(1);                        // error: add() is not const
}

A const object — or a const Counter&, which is how most objects arrive in a function — may only call member functions marked const. The standard library follows this throughout: size(), at(), front() and find() all have const versions, so a const std::vector<int>& is fully readable. Module 8 lesson 3 covers const members in depth, including mutable.

Why const is contagious — and worth it

Suppose Counter::get were not const. Then report fails to compile, and so does every function that takes const Counter& and calls get, and every const auto& loop over a container of Counters. The fix is to add const where it belongs (on get) — or to strip const from every parameter on the way up, which lies to every caller.

That is the contagion, and it points in a good direction. Write every read-only parameter as const T&, every non-mutating member function as const, every read-only pointer as const T*, and the compiler proves, for free and forever, which code changes what: when something is modified unexpectedly, the suspects are exactly the non-const code. Retrofitting const onto a codebase that ignored it is painful, which is why it is a day-one habit rather than a cleanup.

const_cast is a smell

const int limit = 100;
const_cast<int&>(limit) = 200;    // undefined behaviour: limit was declared const

const_cast removes const from a pointer or reference. Writing through the result is defined only if the object was not declared const — limit may live in read-only memory, or the compiler may have folded 100 into every use. The one legitimate use is calling a const-incorrect legacy API that you know does not write; in your own code, a const_cast means a signature is wrong.

Reading harder declarations

DeclarationReads as
const char* spointer to const char — a read-only C string
const char* const* argvpointer to const pointer to const char
int* const& rreference to a const pointer to int
const int* p[3]array of 3 pointers to const int
int (*p)[3]pointer to an array of 3 int

Right to left, parentheses first. const T*, const T& and T* const cover almost everything you write.

Pitfalls

  • The const on the wrong side. int* const when you meant const int* compiles and protects the wrong thing.
  • Believing const int* freezes the object. It restricts your view; the object may still change under you.
  • const auto versus const auto&. for (const auto x : v) copies each element; const auto& does not.
  • Non-const getters. One missing const on a member function blocks every const user of the class.

Key takeaways

  • Read declarations right to left: const left of * is the pointee, right of * is the pointer.
  • const T* and const T& describe your access; the object can still change through other names.
  • Adding const is implicit; removing it is const_cast, and writing through it to a truly const object is undefined behaviour.
  • const member functions are what make const T& parameters usable; mark every non-mutating one.
  • Const correctness spreads by construction, which is exactly why it must start on day one.

Common questions

What does const correctness mean in C++?

Const correctness means marking everything that does not modify an object as const — read-only parameters as const T&, non-mutating member functions as const, read-only pointers as const T* — so the compiler proves which code can change what. It spreads through a codebase, which is why it is a day-one habit rather than a cleanup.

Is const int* the same as int const*?

Yes. Both declare a pointer to a const int; they differ only in style. What matters is which side of the * the const sits on, and const T* is the more common spelling.

Does a pointer to const make the object constant?

No. const int* p only forbids writing through p. The object itself may still be modified through its own name or another non-const pointer, and p will see the new value. const on a pointer or reference describes your view, not the thing.

When is const_cast safe to use?

Writing through the result of const_cast is defined only when the original object was not declared const; casting the const off a truly const object and writing is undefined behaviour. The legitimate use is calling a legacy API that is not const-correct but does not write. In your own code, a const_cast means a signature is wrong.

Mark as readSign in to keep your place.

C++ Const Correctness: Pointer to Const vs Const Pointer

Read C++ declarations right to left: const before the asterisk protects the pointee, after it the pointer. Const parameters and const member functions.

  • Course: C++ study plan
  • Module: Arrays, pointers and references
  • Kind: Lesson
  • Reading time: 13 min
  • Runtime: Clang 18 · C++20

What is the difference between const int* and int* const?

const int* p is a pointer to a const int: you cannot change the value through p, but p can point elsewhere. int* const p is a const pointer to an int: the value can change, but p cannot be reseated. Read C++ declarations right to left — const left of the * applies to the pointee, right of it to the pointer; const int* const locks both.

Lesson

const is the promise "this code does not modify that object", and the compiler enforces it. Placed on a parameter it tells the caller their object is safe; placed on a member function it tells a const object which operations are allowed; placed on a pointer it says whether you may change the pointee, the pointer, or neither. This lesson settles the three pointer placements and the right-to-left reading rule, the conversions const allows and forbids, why the property spreads through a codebase, and why const_cast is a warning sign rather than a tool.

Three placements, one reading rule

int x = 1, y = 2;

const int* a = &x;         // pointer to const int
int const* b = &x;         // the same type, written the other way round
int* const c = &x;         // const pointer to int
const int* const d = &x;   // const pointer to const int

Read each declaration from the name, right to left:

DeclarationReads as*p = 5p = &y
const int* pp is a pointer to a const interrorok
int const* pthe sameerrorok
int* const pp is a const pointer to an intokerror
const int* const pp is a const pointer to a const interrorerror

The rule that generates the table: const left of the * applies to what is pointed at; const right of the * applies to the pointer itself. const int* and int const* differ only in style; const T* is what most code writes. The same reading works for references — const int& r is "r is a reference to a const int" — and there is no int& const, because a reference cannot be reseated anyway.

int* const is rare in practice: a pointer you never reseat is usually a reference. const int* is everywhere — every "look but do not touch" parameter and every begin/end pair over data you only read.

What const on the pointee actually promises

int x = 1;
const int* p = &x;
// *p = 2;               // error: assignment of read-only location
x = 2;                   // fine: x itself is not const
std::cout << *p << '\n'; // 2 — p sees the change

const int* restricts your access through p, not the object: anyone holding x, or an int* to it, may still write, and p observes the new value. const on a pointer or reference describes the view, not the thing.

Conversions: adding const is free, removing it is a cast

int x = 1;
int* p = &x;
const int* cp = p;               // fine: int* → const int* adds a restriction
// int* q = cp;                  // error: would drop the restriction silently
int* q = const_cast<int*>(cp);   // compiles, and is the smell discussed below

T* converts implicitly to const T*, T& to const T&, and a std::vector<int> argument binds to a const std::vector<int>& parameter. Going the other way needs const_cast, on purpose, in writing. This asymmetry is the design: a function that takes const T& can be called with anything, and it cannot leak a non-const handle out.

Parameters

Module 4 gave the three choices — T, T&, const T& — and const is what makes the third one honest. A const std::string& parameter costs one pointer to pass, accepts a literal (a temporary binds to it, lesson 4), and guarantees the caller's string comes back unchanged:

int count_char(const std::string& text, char c) {
    int n = 0;
    for (char ch : text) {
        if (ch == c) ++n;
    }
    return n;
}

A top-level const on a by-value parameter — void f(const int n) — protects only the function's own copy; it means nothing to callers.

const member functions — a preview

struct Counter {
    int value = 0;
    int get() const { return value; }   // promises not to modify *this
    void add(int n) { value += n; }     // may modify
};

void report(const Counter& c) {
    std::cout << c.get() << '\n';       // OK: get() is const
    // c.add(1);                        // error: add() is not const
}

A const object — or a const Counter&, which is how most objects arrive in a function — may only call member functions marked const. The standard library follows this throughout: size(), at(), front() and find() all have const versions, so a const std::vector<int>& is fully readable. Module 8 lesson 3 covers const members in depth, including mutable.

Why const is contagious — and worth it

Suppose Counter::get were not const. Then report fails to compile, and so does every function that takes const Counter& and calls get, and every const auto& loop over a container of Counters. The fix is to add const where it belongs (on get) — or to strip const from every parameter on the way up, which lies to every caller.

That is the contagion, and it points in a good direction. Write every read-only parameter as const T&, every non-mutating member function as const, every read-only pointer as const T*, and the compiler proves, for free and forever, which code changes what: when something is modified unexpectedly, the suspects are exactly the non-const code. Retrofitting const onto a codebase that ignored it is painful, which is why it is a day-one habit rather than a cleanup.

const_cast is a smell

const int limit = 100;
const_cast<int&>(limit) = 200;    // undefined behaviour: limit was declared const

const_cast removes const from a pointer or reference. Writing through the result is defined only if the object was not declared const — limit may live in read-only memory, or the compiler may have folded 100 into every use. The one legitimate use is calling a const-incorrect legacy API that you know does not write; in your own code, a const_cast means a signature is wrong.

Reading harder declarations

DeclarationReads as
const char* spointer to const char — a read-only C string
const char* const* argvpointer to const pointer to const char
int* const& rreference to a const pointer to int
const int* p[3]array of 3 pointers to const int
int (*p)[3]pointer to an array of 3 int

Right to left, parentheses first. const T*, const T& and T* const cover almost everything you write.

Pitfalls

  • The const on the wrong side. int* const when you meant const int* compiles and protects the wrong thing.
  • Believing const int* freezes the object. It restricts your view; the object may still change under you.
  • const auto versus const auto&. for (const auto x : v) copies each element; const auto& does not.
  • Non-const getters. One missing const on a member function blocks every const user of the class.

Key takeaways

  • Read declarations right to left: const left of * is the pointee, right of * is the pointer.
  • const T* and const T& describe your access; the object can still change through other names.
  • Adding const is implicit; removing it is const_cast, and writing through it to a truly const object is undefined behaviour.
  • const member functions are what make const T& parameters usable; mark every non-mutating one.
  • Const correctness spreads by construction, which is exactly why it must start on day one.

Common questions

What does const correctness mean in C++?

Const correctness means marking everything that does not modify an object as const — read-only parameters as const T&, non-mutating member functions as const, read-only pointers as const T* — so the compiler proves which code can change what. It spreads through a codebase, which is why it is a day-one habit rather than a cleanup.

Is const int* the same as int const*?

Yes. Both declare a pointer to a const int; they differ only in style. What matters is which side of the * the const sits on, and const T* is the more common spelling.

Does a pointer to const make the object constant?

No. const int* p only forbids writing through p. The object itself may still be modified through its own name or another non-const pointer, and p will see the new value. const on a pointer or reference describes your view, not the thing.

When is const_cast safe to use?

Writing through the result of const_cast is defined only when the original object was not declared const; casting the const off a truly const object and writing is undefined behaviour. The legitimate use is calling a legacy API that is not const-correct but does not write. In your own code, a const_cast means a signature is wrong.

Exercises

A const-correct pipeline

Read n (1 ≤ n ≤ 100) integers, then from and to. Write three functions with exactly these signatures — int count_matching(const std::vector<int>& values, int target), void replace_all(std::vector<int>& values, int from, int to) and void print(const std::vector<int>& values) — and use them: print how many elements equal from, replace every one of them with to, and print the vector. Only replace_all may take a non-const reference.

Input: n, then n integers, then from to. Output: count=<k>, then the vector space-separated on one line.

Example: input 5 1 2 1 3 1 1 9 →

count=3
9 2 9 3 9

Which const is which

Read n (0 ≤ n ≤ 100) integers into a std::vector<int>, then a target. Write int index_of(const int* begin, const int* end, int target) — a walking pointer over elements you may not modify, returning the index as a pointer difference or -1 — and void double_all(int* const begin, int* const end) — two pointers you may not reseat, over elements you may modify, so the walk needs a local copy of begin. Print the index of target, double every element, and print the vector.

Input: n, then n integers, then target. Output: index=<i or -1>, then the doubled values space-separated (an empty line when n is 0).

Example: input 4 3 5 7 5 5 →

index=1
6 10 14 10

In this module: Arrays, pointers and references

← References — an alias with a rulebook · Multidimensional data — grids, layout and neighbours →