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:
| Declaration | Reads as | *p = 5 | p = &y |
|---|---|---|---|
const int* p | p is a pointer to a const int | error | ok |
int const* p | the same | error | ok |
int* const p | p is a const pointer to an int | ok | error |
const int* const p | p is a const pointer to a const int | error | error |
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
| Declaration | Reads as |
|---|---|
const char* s | pointer to const char — a read-only C string |
const char* const* argv | pointer to const pointer to const char |
int* const& r | reference 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* constwhen you meantconst 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 autoversusconst auto&.for (const auto x : v)copies each element;const auto&does not.- Non-const getters. One missing
conston a member function blocks everyconstuser of the class.
Key takeaways
- Read declarations right to left:
constleft of*is the pointee, right of*is the pointer. const T*andconst T&describe your access; the object can still change through other names.- Adding
constis implicit; removing it isconst_cast, and writing through it to a truly const object is undefined behaviour. constmember functions are what makeconst T¶meters 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 9Which 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 10In this module: Arrays, pointers and references
- Built-in arrays and std::array
- Pointers — addresses, nullptr and the arrow
- Pointer arithmetic and arrays
- References — an alias with a rulebook
- Const correctness — reading declarations right to left (this lesson)
- Multidimensional data — grids, layout and neighbours
- Checkpoint — Arrays, pointers and references
← References — an alias with a rulebook · Multidimensional data — grids, layout and neighbours →