const and constness
A promise to the compiler and to the reader, enforced at compile time.
By the end of this chapter you can
- Read a const declaration right-to-left without guessing
- Explain the difference between a const pointer and a pointer to const
- Decide where const belongs in an interface
const means this will not be modified through this name. That phrasing is
deliberate and every word earns its place: it is a promise attached to a name,
not a property of the underlying object, and the compiler enforces it at compile
time and then forgets about it.
It costs nothing at run time. What it buys is that a whole category of mistake becomes a compile error, and that a reader can tell from a signature alone whether a function will change their data.
The basic promise
int main() {
const int limit = 100;
limit = 200; // error: assignment of read-only variable
}The error arrives at compile time, before the program has a chance to be wrong.
const variables must be initialised where they are declared, for the obvious
reason that there is no later opportunity.
Reading declarations right to left
The rule that removes all guesswork: read from the variable’s name, outwards, right to left.
#include <iostream>
int main() {
int a = 1, b = 2;
// p is a pointer to a const int
const int* p = &a;
p = &b; // fine: p itself is not const
// *p = 5; // error: cannot write through p
// q is a const pointer to an int
int* const q = &a;
*q = 5; // fine: what q points at is not const
// q = &b; // error: q itself is const
// r is a const pointer to a const int
const int* const r = &a;
std::cout << "a = " << a << ", *p = " << *p << ", *r = " << *r << '\n';
}const int* p→pis a pointer to an int that is const. The pointee is protected.int* const q→qis a const pointer to an int. The pointer is protected.const int* const r→ both.
int const* p is identical to const int* p — when const appears leftmost,
it binds to the type on its right. Some people write the const on the right
consistently (int const*) precisely so that right-to-left reading always works
without that exception. Either is fine; be consistent.
const int* p is the one you will write constantly, because it is how a
function says I will read this and not change it.
const references, and why they are everywhere
#include <iostream>
#include <string>
#include <vector>
// Reads only. No copy, and the compiler enforces the promise.
std::size_t total_length(const std::vector<std::string>& items) {
std::size_t total = 0;
for (const std::string& item : items) {
total += item.size();
// item += "!"; // error: item is a const reference
}
return total;
}
int main() {
std::vector<std::string> words{"alpha", "beta", "gamma"};
std::cout << total_length(words) << '\n';
// A const reference also binds to a temporary, which a non-const one cannot.
std::cout << total_length({"one", "two"}) << '\n';
}That last line is a genuine practical advantage, not a curiosity. const T&
binds to temporaries; T& does not. A function taking std::string& cannot be
called with a literal, so making a read-only parameter non-const gratuitously
restricts your callers.
#include <string>
void shout(std::string& text) { text += "!"; }
int main() {
shout("hello"); // error: cannot bind a non-const lvalue ref to a temporary
}The error is the compiler protecting you: modifying a temporary that is about to be destroyed is almost always a mistake, so the language will not let a non-const reference bind to one.
const member functions
On a class, const after the parameter list promises that calling this function
does not modify the object.
#include <iostream>
#include <string>
class Account {
public:
explicit Account(int start) : balance_(start) {}
int balance() const { return balance_; } // promises not to modify
void deposit(int amount) { balance_ += amount; }
private:
int balance_;
};
void audit(const Account& account) {
std::cout << "balance: " << account.balance() << '\n';
// account.deposit(10); // error: deposit is not const
}
int main() {
Account a{100};
a.deposit(50);
audit(a);
}This is what makes const& parameters useful rather than merely safe. Inside
audit, the object is const, so only the const member functions are callable —
and that is enforced, not documented.
What const does not promise
Three limits worth knowing, because over-trusting const causes its own bugs.
It is shallow. A const object’s members are const, but if a member is a
pointer, const applies to the pointer, not to what it points at:
#include <iostream>
struct Holder {
int* data;
};
int main() {
int value = 1;
const Holder h{&value};
// h.data = nullptr; // error: the pointer is const
*h.data = 99; // fine: what it points at is not
std::cout << "value = " << value << '\n';
}const Holder made h.data a int* const, not a const int* const. If you
want the deep promise, the member has to be declared const int*.
It does not mean “stored in read-only memory”. A const local is an
ordinary object on the stack; const is a rule about names, checked by the
compiler. Only objects with static storage and constant initialisation are
likely to end up in a genuinely read-only page.
It can be cast away. const_cast removes it, and doing so is legal — but
writing through the result is undefined behaviour if the object was originally
declared const:
#include <iostream>
void legacy_api(char* text) { std::cout << "legacy got: " << text << '\n'; }
int main() {
char buffer[] = "modifiable";
const char* view = buffer; // we chose to view it as const
// The underlying object is not const, so casting back is well defined.
legacy_api(const_cast<char*>(view));
}That is the one legitimate use: interfacing with an old API that takes a
non-const pointer but does not actually modify. If the object itself was
declared const, casting the constness away and writing is undefined behaviour,
and the optimizer will happily assume you did not.
constexpr, briefly
const says not modified through this name. constexpr says computable at
compile time — a stronger and different claim.
#include <array>
#include <iostream>
int runtime_value() { return 5; }
int main() {
const int a = runtime_value(); // const, but not known until run time
constexpr int b = 5; // known at compile time
std::array<int, b> fixed{}; // fine: b is a constant expression
// std::array<int, a> broken{}; // error: a is not
std::cout << "a = " << a << ", array size = " << fixed.size() << '\n';
}Every constexpr variable is implicitly const, but not every const variable
is constexpr. Chapter 5.5 covers compile-time computation properly; for now,
use constexpr for genuine compile-time constants and const for everything
else.
Where to put const
A short set of defaults that will serve you for years:
- Parameters you only read, and that are larger than a pointer:
const T&. - Parameters you only read, and that are small: plain
Tby value — addingconstto a by-value parameter in a declaration is noise, since the caller’s object is untouched either way. - Member functions that do not modify the object:
const, always. - Local variables you will not reassign:
constis cheap and documents intent, though opinions differ on whether it earns its keep everywhere. - Return types: do not return
const Tby value; it is pointless and it blocks moves, making your callers slower.