Const correctness hardens a class by letting the compiler prove that certain operations cannot change the object. A [[const]] member function promises to leave the object exactly as it found it, and once that promise is in the signature the compiler enforces it for every caller. The result is a class whose read-only operations are obvious from the call site rather than hidden in the documentation.
The const member function
Putting const after the parameter list of a member function means the function does not modify the object.
#include
#include
class Vehicle {
public:
explicit Vehicle(const std::string& reg, int miles)
: registration_(reg), mileage_(miles) {}
std::string summary() const {
return registration_ + " (" + std::to_string(mileage_) + " miles)";
}
void add_mileage(int miles) {
mileage_ += miles;
}
private:
std::string registration_;
int mileage_;
};
int main() {
const Vehicle van("VN-4471", 18200);
std::cout << van.summary() << '\n';
// van.add_mileage(100); // does not compile: van is const
Vehicle truck("TR-0093", 74000);
truck.add_mileage(120);
std::cout << truck.summary() << '\n';
return 0;
}The output shows the const object being read and the mutable one being changed:
VN-4471 (18200 miles)
TR-0093 (74120 miles)The commented-out line is a compile error, and the message says exactly why: you cannot call a non-const member function on a const object.
Why a const object is useful
A const object is one that cannot change after it is created. Any function that reads it can then be called safely, and the compiler will not let the object be modified behind your back.
#include
#include
class Vehicle {
public:
explicit Vehicle(const std::string& reg, int miles)
: registration_(reg), mileage_(miles) {}
const std::string& registration() const {
return registration_;
}
int mileage() const {
return mileage_;
}
bool is_longer_than(int threshold) const {
return mileage_ > threshold;
}
private:
std::string registration_;
int mileage_;
};
void print_report(const Vehicle& vehicle) {
std::cout << vehicle.registration() << ": " << vehicle.mileage() << " miles\n";
}
int main() {
const Vehicle van("VN-4471", 18200);
print_report(van);
std::cout << "over 10000 miles: " << van.is_longer_than(10000) << '\n';
return 0;
}The output shows the const object flowing through a const-reference parameter into const member functions:
VN-4471: 18200 miles
over 10000 miles: 1Every function in that chain is const, so nothing along the way can modify the vehicle. The compiler proved it, rather than a reviewer having to check.
const propagates through the interface
Once you have a const object, every member function you call on it must be const. This is a feature: it forces the read-only path through your code to be genuinely read-only, and it makes the const-ness of a whole API visible in one glance.
#include
#include
#include
class Vehicle {
public:
explicit Vehicle(const std::string& reg, int miles = 0)
: registration_(reg), mileage_(miles) {}
const std::string& registration() const {
return registration_;
}
void add_mileage(int miles) {
mileage_ += miles;
}
int mileage() const {
return mileage_;
}
private:
std::string registration_;
int mileage_ = 0;
};
class Fleet {
public:
void add(const Vehicle& vehicle) {
vehicles_.push_back(vehicle);
}
int total_mileage() const {
int total = 0;
for (const Vehicle& vehicle : vehicles_) {
total += vehicle.mileage();
}
return total;
}
std::string first_registration() const {
return vehicles_.empty() ? "" : vehicles_.front().registration();
}
private:
std::vector vehicles_;
};
int main() {
Fleet fleet;
Vehicle van("VN-4471", 18200);
fleet.add(van);
std::cout << "first: " << fleet.first_registration() << '\n';
std::cout << "total: " << fleet.total_mileage() << " miles\n";
return 0;
}The output shows both const queries on the fleet:
first: VN-4471
total: 18200 milesThe rules of const
- A const member function is declared with
constafter the parameter list. - Inside a const member function, every data member is treated as const, so none of them can be modified.
- A const return type such as
const std::string&is a promise not to modify the object it refers to. - A const reference parameter lets the function read a large object without copying it and without being able to change it.
- Mutable data members can be changed inside a const function by declaring them
mutable. Use this only for genuine caches.
mutable and const_cast
Sometimes a const function needs to change something, typically a cached value that does not affect the object's logical state. The mutable keyword says exactly that.
#include
#include
class Vehicle {
public:
explicit Vehicle(const std::string& reg) : registration_(reg) {}
const std::string& registration() const {
return registration_;
}
void add_mileage(int miles) {
mileage_ += miles;
cache_valid_ = false;
}
int mileage() const {
if (!cache_valid_) {
cached_mileage_ = mileage_;
cache_valid_ = true;
}
return cached_mileage_;
}
private:
std::string registration_;
int mileage_ = 0;
mutable int cached_mileage_ = 0;
mutable bool cache_valid_ = false;
};
int main() {
Vehicle van("VN-4471");
van.add_mileage(18200);
std::cout << "mileage: " << van.mileage() << '\n';
return 0;
}The output shows the const query working through the cache:
mileage: 18200std::const_cast exists too, but it removes constness and is almost always the wrong answer. If a function needs to modify the object, make it non-const and let the caller decide.
Quick reference
constafter the parameter list makes a member function read-only.- A const object can only call const member functions.
- Const correctness propagates: read-only callers force read-only functions all the way down.
- Use
const T¶meters to avoid copies without allowing modification. - A
constreturn type promises not to modify what it refers to. mutableallows a const function to update a cache;std::const_castshould be a last resort.
Try it yourself: write a [[Vehicle]] class with a mileage and a [[miles_until_service]] query, and mark the query [[const]]. Then create a [[const Vehicle]] and try to modify it, and read the error. Next, change the query to non-const and see the error disappear — that difference is exactly what const correctness buys you.