Const correctness and encapsulation
Classes and object lifetime12 min readLesson 4 of 4
4.1 Keep the rules with the data
Encapsulation keeps an object's data consistent by routing every change through member functions. The class knows which combinations of values are valid, and the public interface offers operations that preserve them. A caller cannot set a balance directly and skip the withdrawal rule.
The public part of a class is a contract. It should be small enough to understand and stable enough that callers can rely on it. Data members usually stay private. Accessors expose values for reading, and named operations handle changes that need checks.
#include <iostream>
class Account {
public:
explicit Account(int balance) : balance_(balance > 0 ? balance : 0) {}
bool withdraw(int amount) {
if (amount <= 0 || amount > balance_) return false;
balance_ -= amount;
return true;
}
int balance() const { return balance_; }
private:
int balance_;
};
int main() {
Account account(100);
std::cout << std::boolalpha << "First: " << account.withdraw(30) << "\n";
std::cout << "Second: " << account.withdraw(200) << "\nBalance: " << account.balance() << "\n";
return 0;
}Output
First: true Second: false Balance: 70
4.1.1 Access levels and friends
public members are part of the interface. private members are reachable only from the class's own member functions and friends. protected members are also reachable from derived classes, which the next module covers. A class starts private; a struct starts public.
A friend declaration gives a named function or class access to private members. It is useful for a comparison function that needs two objects' internals, or for an operator that cannot be a member. The class grants friendship; the friend cannot take it. Keep it rare, because it widens the interface beyond the member functions.
#include <iostream>
class Account {
public:
explicit Account(int balance) : balance_(balance) {}
friend bool richer(const Account& a, const Account& b);
private:
int balance_;
};
bool richer(const Account& a, const Account& b) {
return a.balance_ > b.balance_;
}
int main() {
Account first(80);
Account second(120);
std::cout << std::boolalpha << "Second richer: " << richer(second, first) << "\n";
return 0;
}Output
Second richer: true
4.1.2 Const member functions
A const member function promises not to change the object's visible state. The compiler enforces the promise by treating the object as const inside the function, so an assignment to a data member fails. Const objects and const references can call only the functions that carry the marker.
Mark getters, display functions, and comparisons const when they do not change the object. The marker also lets the same object be passed to functions that take a const reference. A const-correct class is easier to use in larger programs because callers do not have to guess which functions are safe to call on shared data.
#include <iostream>
class Row {
public:
explicit Row(int value) : value_(value) {}
int& first() { return value_; }
const int& first() const { return value_; }
private:
int value_;
};
int main() {
Row row(5);
row.first() = 9;
const Row locked(3);
std::cout << row.first() << " " << locked.first() << "\n";
return 0;
}Output
9 3
4.1.3 Mutable and logical constness
Sometimes a const member function needs to update bookkeeping that callers cannot observe. A cache, a hit counter, or a lazily computed value can change while the object's logical state stays the same. The mutable keyword allows exactly that: a mutable member can change inside a const member function.
Use mutable for data that supports the function's result and is not part of the value the object represents. A member that holds the object's main state should stay non-mutable. If a const function changes something a caller can see, the function is not really const, and the design should change instead.
#include <iostream>
class Sensor {
public:
explicit Sensor(int value) : value_(value) {}
int read() const { ++reads_; return value_; }
int reads() const { return reads_; }
private:
int value_;
mutable int reads_ = 0;
};
int main() {
Sensor sensor(21);
std::cout << "Reading: " << sensor.read() << "\n";
std::cout << "Reads: " << sensor.reads() << "\n";
return 0;
}Output
Reading: 21 Reads: 1
4.1.4 Do not hand out the internals
A getter that returns a non-const reference gives callers direct access to a private member. They can change the vector, the string, or the number without going through the class, and the class cannot check the result. The private keyword then protects the name while the value can change without checks.
Return a copy when the value is small, or a const reference when it is large and read-only. If callers must edit the collection, offer named operations such as add_member, remove_member, or replace_member. The class keeps control of its rules and still provides the access callers need.
#include <iostream>
#include <string>
#include <vector>
class Team {
public:
void add(const std::string& name) { members_.push_back(name); }
const std::vector<std::string>& members() const { return members_; }
private:
std::vector<std::string> members_;
};
int main() {
Team team;
team.add("Ada");
team.add("Ravi");
const std::vector<std::string>& names = team.members();
std::cout << "Members: " << names.size() << "\n";
return 0;
}Output
Members: 2
4.1.5 Common mistakes
- Making data members public because a getter and a setter feel like extra work.
- Forgetting
conston a getter, then discovering that a const object cannot use it. - Returning a non-const reference from a getter.
- Marking a function
constand then changing visible state throughmutable. - Adding a friend to avoid designing a proper public operation.
- Giving every member its own getter and setter, which exposes the representation without protecting anything.
4.1.6 Practice
- Write a
Counterclass with a private count and publicincrement,reset, andvaluefunctions. - Make
valueconst and call it on a const object. - Add a friend function that compares two counters by their private counts.
- Add a
mutableread counter to a const getter and print it after two reads. - Replace a public vector member with a private one and a const reference getter.
