The access specifiers [[public]], [[private]] and [[protected]] in C++ decide who is allowed to touch a class's internals. What they actually protect is not the data but the *invariant*: the rule that keeps a class's state valid. A class with public members has no invariant, because anyone can put any value into them, and a class with private members and public accessors can be changed later without breaking anybody.
The three levels of access
A class starts as private by default, and struct starts as public. That is the only difference between a class and a struct in C++.
#include
class Vehicle {
private:
int odometer; // only this class may touch it
protected:
int service_count; // this class and its derived classes
public:
std::string registration; // everyone may touch it
};
int main() {
Vehicle van;
van.registration = "VN-4471";
// van.odometer = 100; // does not compile: odometer is private
std::cout << "registration: " << van.registration << '\n';
return 0;
}The output shows the public member being set and used:
registration: VN-4471The commented-out line is a compile error, and the error message names the member and the class, which is one of the friendliest diagnostics the language produces.
Why private beats public
A public member is a promise that the value will stay sensible forever. A private member is not a promise at all.
#include
class Vehicle {
public:
Vehicle() : odometer(0), service_due(10000) {}
void service() {
odometer = 0;
service_due = 10000;
}
int miles_until_service() const {
return service_due - odometer;
}
void drive(int miles) {
if (miles < 0) {
return;
}
odometer += miles;
}
private:
int odometer;
int service_due;
};
int main() {
Vehicle van;
van.drive(9000);
std::cout << "miles until service: " << van.miles_until_service() << '\n';
van.drive(2000);
std::cout << "miles until service: " << van.miles_until_service() << '\n';
van.service();
std::cout << "after service: " << van.miles_until_service() << '\n';
return 0;
}The output shows the class controlling its own state:
miles until service: 1000
miles until service: -1000
after service: 10000Nobody outside the class can set the odometer backwards or set it to a negative number, because there is no way to reach it. The class can be changed internally — the units, the service interval, the way the counter is stored — and no caller will notice.
Accessors and the naming convention
The usual pattern is a private member plus a public getter and, where it makes sense, a setter.
#include
#include
class Vehicle {
public:
explicit Vehicle(const std::string& reg) : registration_(reg), mileage_(0) {}
const std::string& registration() const {
return registration_;
}
int mileage() const {
return mileage_;
}
void add_mileage(int miles) {
if (miles > 0) {
mileage_ += miles;
}
}
private:
std::string registration_;
int mileage_;
};
int main() {
Vehicle van("VN-4471");
van.add_mileage(1200);
van.add_mileage(-500); // ignored: a negative trip is not possible
std::cout << van.registration() << ": " << van.mileage() << " miles\n";
return 0;
}The trailing underscore on the member name is a widespread convention that keeps it distinct from the accessor of the same name. The output shows the negative distance being rejected:
VN-4471: 1200 milesNotice that add_mileage is better than a setter: it can enforce the rule that mileage only ever increases. A setter would have to accept the caller's judgement, and the caller would have to know the rule.
protected is for inheritance
protected members are private to everyone except the class itself and its derived classes. It exists so a derived class can work with the base class's internals without making them public to the world.
#include
#include
class Vehicle {
public:
explicit Vehicle(const std::string& reg) : registration_(reg) {}
const std::string& registration() const {
return registration_;
}
protected:
void set_registration(const std::string& reg) {
registration_ = reg;
}
private:
std::string registration_;
};
class FleetVehicle : public Vehicle {
public:
FleetVehicle(const std::string& reg, int seats)
: Vehicle(reg), seat_count_(seats) {}
void reassign(const std::string& reg) {
set_registration(reg); // allowed: protected
}
int seats() const {
return seat_count_;
}
private:
int seat_count_;
};
int main() {
FleetVehicle shuttle("SH-22", 12);
shuttle.reassign("SH-23");
std::cout << shuttle.registration() << " with " << shuttle.seats() << " seats\n";
return 0;
}The output confirms the derived class could reach the protected setter:
SH-23 with 12 seatsprotected is the weakest form of encapsulation and the easiest to overuse. Many style guides now recommend avoiding it and using public member functions instead, because a protected member is still part of the class's public contract for every future subclass.
Encapsulation is what makes refactoring possible
The practical payoff is that a private member can be changed freely. If the odometer were stored as an int and you later need it as a long long because a vehicle passes a million miles, you can change it as long as the accessors keep the same signatures. Every caller keeps working.
If the member were public, that change would break every caller, and you would have no way to stop people from depending on the old layout.
Quick reference
classdefaults toprivate,structdefaults topublic.public: anyone may use it.private: only this class.protected: this class and its derived classes.- A private member with public accessors lets you change the implementation later.
- Prefer a method such as
add_mileageover a setter when the class can enforce a rule. protectedis a weak form of encapsulation; prefer public member functions.- Encapsulation is what makes refactoring safe.
Try it yourself: write a [[Vehicle]] class with a public mileage field, then change it to a private field with accessors and update your callers. Next, add a rule that the mileage can never decrease, enforced inside the class. Finally, write a derived class and use a [[protected]] member from it, then try the same access from outside the class to see the compiler refuse.