A function that receives a read-only reference communicates that it will not modify the object through that reference. A const member function lets callers use a class through a const object. These promises make interfaces easier to reason about, but they are more precise than “nothing can ever change.” Another permitted alias can still modify a non-const underlying object, and pointer qualifiers can restrict the pointer itself or the pointed-to data independently.
This guide uses C++17. You will interpret pointer declarations, provide const and non-const access overloads, and design a small read-only report interface. The examples use checked container access and keep referenced objects alive.
Const applies to a particular type layer
In const int* p, the pointed-to integer is read-only through p, but p can be changed to point elsewhere. In int* const p, the pointer value is fixed, but the integer can be modified through it. In const int* const p, both restrictions apply. Reading from the identifier outward helps distinguish these layers.
These restrictions do not establish ownership. A const pointer can still dangle if its target dies. A pointer to const can also observe changes performed through another mutable alias when the underlying object was not originally const. Use const to communicate permitted operations, then separately explain lifetime and sharing. Treat a compiler error about assignment to a const-qualified target as useful evidence that the interface is enforcing a promise.
Const references support observation without copying
Passing a large object by const reference allows a function to read its public const interface without copying the object. It is useful when borrowing is appropriate, but small inexpensive values may remain clearer by value. A const reference parameter does not make asynchronous retention safe; the function must not keep it after the owner's lifetime unless a separate contract permits that.
A returned const reference also borrows from some owner. Returning a reference to a local temporary leaves the caller with a dangling reference, even though it is const. Returning a reference to a member can be appropriate while the enclosing object remains alive and the member remains valid. Document invalidation if later mutation can reallocate or replace the referenced storage.
Const member functions form the read-only interface
A member function with a trailing const can be called on a const object. It treats ordinary member state as read-only through its implicit object access. A non-const member function is not callable through a const object merely because it happens not to modify anything today.
Provide const-qualified observers such as size and status when they genuinely only observe state. If a method returns a reference to internal mutable data, its const overload should return a const reference. Otherwise callers could modify state through a supposedly read-only object. Overloading on const qualification lets the same method name provide writable access for mutable callers and read-only access for const callers, with the compiler selecting the appropriate contract.
Logical constness needs a narrow policy
Sometimes an observer updates an internal cache while leaving the object's externally meaningful value unchanged. The mutable keyword can permit a member to change inside a const member function. This is logical constness, but it should be used carefully and documented. A mutable cache still requires synchronization if several threads can access it.
Do not mark ordinary business fields mutable merely to silence compiler errors. If an operation changes the object's meaningful state, it should normally be non-const. A const method that performs unexpected external effects can also surprise callers even if it does not change a field. Keep observation methods predictable, and explain expensive work or lazy cache construction where it matters.
Casting away const does not establish permission
A const_cast can alter qualification in selected expressions, but writing through it is undefined behavior when the underlying object is genuinely const. Even when the underlying object is non-const, bypassing an API's read-only contract can break its assumptions. Do not use the cast as a routine solution to a design error.
If an API requires writable data, provide an actually writable object with a suitable lifetime. If a legacy function promises not to modify a buffer despite an outdated signature, isolate that compatibility decision and document it rather than scattering casts throughout application code. A well-designed const interface prevents accidental mutation at compile time and makes exceptional unsafe boundaries easier to review.
Official references
C++ Const Correctness and Interfaces Examples
Each example shows the code, the output it produces and what each part does.
Const overloads preserve access restrictions
This complete program uses checked access in a small collection wrapper. A mutable object can modify a value; a const reference can only read it.
The example includes the declaring header for std::move instead of relying on accidental transitive includes. The read-only view shares the same underlying object rather than taking a frozen copy.
#include <cstddef>
#include <iostream>
#include <vector>
#include <utility>
class Quantities {
std::vector<int> values_;
public:
explicit Quantities(std::vector<int> values)
: values_(std::move(values)) { }
int& at(std::size_t index) {
return values_.at(index);
}
const int& at(std::size_t index) const {
return values_.at(index);
}
std::size_t size() const noexcept {
return values_.size();
}
};
int main() {
Quantities quantities({2, 4});
quantities.at(0) = 7;
const Quantities& view = quantities;
std::cout << "First: " << view.at(0) << '\n';
std::cout << "Count: " << view.size() << '\n';
int left = 3;
int right = 8;
const int* reader = &left;
reader = &right;
int* const writer = &left;
*writer = 5;
std::cout << "Read: " << *reader << '\n';
std::cout << "Changed: " << left << '\n';
}First: 7
Count: 2
Read: 8
Changed: 5C++ Const Correctness and Interfaces in Real Projects
Worked exercise: a read-only invoice summary
The task is to make an invoice usable through a const reference while preserving a controlled way to add a line on a mutable invoice. Use explicit observer methods and keep the vector private. The total is recomputed from small bounded demonstration data, avoiding mutable cached state.
#include <iostream>
#include <stdexcept>
#include <vector>
class Invoice {
std::vector<int> cents_;
public:
void add_line(int cents) {
if (cents < 0)
throw std::invalid_argument("negative line");
cents_.push_back(cents);
}
int total() const {
int sum = 0;
for (int cents : cents_)
sum += cents;
return sum;
}
};
void print_invoice(const Invoice& invoice) {
std::cout << "Total cents: " << invoice.total() << '\n';
}
int main() {
Invoice invoice;
invoice.add_line(250);
invoice.add_line(400);
print_invoice(invoice);
}Total cents: 650The print function cannot call add_line through its parameter. The example's two small values keep the int sum within range; a production amount model must additionally define bounded accumulation or checked arithmetic. Const correctness does not replace numeric validation.
C++ Const Correctness and Interfaces FAQ
Does const mean the object can never change?
It restricts operations through the relevant const access path. Another mutable alias can change an underlying non-const object, and selected mutable members can change under logical constness. A genuinely const object must not be modified through a cast. Distinguish read-only access, immutability, ownership and thread safety; none of those additional properties follows automatically from a const reference.
Why need a const overload of a getter?
A const object can call only an applicable const member function. An overload returning a const reference also prevents mutation through the returned access path. The mutable overload can provide writable access when that is an intentional API choice. Returning a writable reference from the const overload would undermine the object's read-only promise and allow callers to bypass controlled mutation.
Is a const reference always better than passing by value?
No. Small values can be clearer and cheap by value, and an owned copy can simplify lifetime. A const reference is useful for borrowing larger objects without copying. It does not extend the owner's lifetime or make retention safe. Choose from cost, ownership and semantics rather than applying a blanket rule to every parameter.
When is mutable appropriate?
A carefully designed internal cache or synchronization member can need mutation while preserving logical constness. That should remain an implementation detail under a clear policy. Mutable is not a substitute for declaring a state-changing operation non-const. If access can overlap, the cache still needs synchronization. Tests should verify the observable value and thread-safety contract separately.
C++ Const Correctness and Interfaces Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
A cast removes const from an object originally declared const. What happens if code writes through the result?
C++ Const Correctness and Interfaces Quiz
2 questions to check what you learned.
What does const int* p restrict?
- Reassigning p to another integer.
- Modifying the integer through p.
- Modifying the same integer through every other alias.
- Reading the integer's value.
A const collection's at method returns a reference. Which return type preserves read-only access?
- T&&, to permit moving from every const collection.
- T&
- const T&
- T&, because a const member always makes its returned reference const.