Lesson 5 of 6
A pointer involves two objects: the pointer itself, and the thing it points at. Either can be
const, or both. That gives three useful combinations, and the syntax is famously confusing
until you learn one rule.
The rule
Read the declaration right to left, and const applies to whatever is immediately to its
left — unless there is nothing to its left, in which case it applies to what is on its right.
That single sentence decodes every case:
int* p; // pointer to int
const int* p; // pointer to const int (nothing left of const -> applies right)
int const* p; // identical to the line above
int* const p; // const pointer to int (int* is left of const)
const int* const p; // const pointer to const int
Notice const int* and int const* mean the same thing. The second is arguably more consistent
with the rule, but the first is far more common in real code, so you should recognise both.
Pointer to const: the data is frozen
#include <iostream>
int main() {
int x = 1;
int y = 2;
const int* p = &x; // cannot write through p
// *p = 99; // error: assignment of read-only location
p = &y; // but p itself can move
std::cout << *p << "\n";
y = 50; // and the object can still change by other means
std::cout << *p << "\n";
return 0;
}
2
50
That last line matters. const int* is a promise about what you may do through this pointer,
not a promise that the object never changes. y is not const; the pointer just cannot be used to
modify it.
This is the common case, and it is what you see across the standard library: a function taking
const T* (or const T&) is saying "I will look, not touch."
const pointer: the pointer is frozen
#include <iostream>
int main() {
int x = 1;
int y = 2;
int* const p = &x; // p always points at x
*p = 99; // the data can change
std::cout << x << "\n";
// p = &y; // error: assignment of read-only variable 'p'
std::cout << (p == &x) << " " << (p == &y) << "\n";
return 0;
}
99
1 0
A const pointer must be initialised at declaration, for the same reason a const int must be —
there is no later opportunity to set it.
This one is rarer. In most code where you would reach for it, a reference is a better fit:
int& r = x; is a non-null, non-re-seatable alias, which is int* const with the null case
removed.
Both
#include <iostream>
int main() {
int x = 42;
const int* const p = &x;
std::cout << *p << "\n"; // reading is all you may do
// *p = 1; // error
// p = nullptr; // error
return 0;
}
42
Conversions: which way is allowed
You may always add const. You may not silently remove it:
#include <iostream>
void look(const int* p) {
std::cout << *p << "\n";
}
int main() {
int x = 5;
look(&x); // int* -> const int* : fine, adding a restriction
const int y = 7;
// int* q = &y; // error: adding const is fine, dropping it is not
const int* r = &y; // this is the only correct type for &y
std::cout << *r << "\n";
return 0;
}
5
7
This asymmetry is what makes const useful. A function taking const int* accepts both mutable
and constant data, so const on a parameter widens what you can call it with while narrowing
what the function may do.
const_cast exists to strip const, and you should treat it as a red flag. Using it to write to
an object that was genuinely declared const is undefined behaviour.
const and the array-of-pointers trap
One place the right-to-left rule earns its keep:
#include <iostream>
int main() {
int a = 1, b = 2;
// An array of 2 pointers-to-const-int.
const int* arr[2] = {&a, &b};
std::cout << *arr[0] << " " << *arr[1] << "\n";
arr[0] = &b; // the array slots are mutable
std::cout << *arr[0] << "\n";
a = 100; // and the ints themselves are not const
std::cout << *arr[1] << " " << a << "\n";
return 0;
}
1 2
2
2 100
Nothing here is actually frozen except your ability to write *arr[i] = .... Work through the
declaration right to left when it looks ambiguous and it stops being ambiguous.
Try it
Write int sum(const int* values, std::size_t count) that adds up an array without being able
to modify it. Then try adding values[0] = 0; inside and read the error. Finally, declare a
const int* const parameter and say what each const is doing.
Checkpoint
- Read right to left;
constbinds to what is on its left, or to its right if nothing is there. const int*— the data is read-only through this pointer; the pointer can move.int* const— the pointer is fixed; the data can change.const int* const— both.const int*andint const*are the same type.- Adding
constis an implicit conversion; removing it needsconst_castand usually a rethink. const T*on a parameter promises the caller you will not modify their data.
Questions about this lesson
Log in to ask a question.