Encapsulation and Access Specifiers
A guide to encapsulation, class invariants, and controlling data access with public
and private
specifiers.
We've now successfully abstracted our Monster objects into a class. This is keeping our code nice and organized.
The next step we want to consider is how we keep the objects of our classes in a valid state.
Class Invariants
To make our classes useful, we generally want to establish some rules that users of our class can rely on.
For example, consider a simple class like this:
class Monster {
public:
int Health { 150 };
};
It doesn't make sense for a Health
value to be negative, so it would be useful to establish that as a rule that developers using our class can rely on.
A rule, such as "the Health
of a Monster
is never negative" is sometimes referred to as a class invariant.
We can attempt to set up the "Health
is never negative" invariant by providing a TakeDamage()
function that implements this rule:
class Monster {
public:
int Health { 150 };
void TakeDamage(int Damage) {
Health -= Damage;
if (Health < 0) { Health = 0; }
};
};
Our rule is now guaranteed if consumers only ever use the TakeDamage()
function. But they can just bypass our rule by setting Health
directly, so we're not done:
#include <iostream>
using namespace std;
class Monster {
public:
int Health{150};
void TakeDamage(int Damage){/*...*/};
}
int main(){
Monster Goblin;
Goblin.Health -= 200;
cout << "Health: " << Goblin.Health << " :(";
}
Health: -50 :(
Encapsulation
In programming, the process of encapsulation involves:
- Bundling data and the functions that act on that data into the same object
- Hiding the inner workings so consumers can only interact with the object in a controlled way
We have implemented the first step of encapsulation, by bundling our variables and functions into a class.
However, we have not implemented the second step. Consumers can see all the inner workings of our object.
This is making it unclear how they're supposed to use our object - "do I use TakeDamage()
or do I modify Health
?". And, as we've seen, the lack of encapsulation is making it impossible for us to implement our class invariants.
Test your Knowledge
Understanding Encapsulation
What is an example of encapsulation?
Public and Private Class Members
To express this in C++ terminology, we want to have part of our class be public
and part of it to be private
.
The parts of the class that we want external code to use will be public
. This will be the friendly, external-facing interface.
The parts that we don't want people messing with will be private
.
We saw in the class definition above, we already have the word public
in our code:
class Monster {
public:
int Health { 150 };
void TakeDamage(int Damage){/*...*/};
};
In classes, all the members are private by default. What we were doing here was making everything public. Any code that creates a new object from our class can access and change everything on that object.
Let's update our class to introduce a private
section, and move our Health
variable there:
class Monster {
public:
void TakeDamage(int Damage){/*...*/};
private:
int Health { 150 };
};
Private members of a class can still be modified by the functions of the class. Our class functions like TakeDamage()
will be able to modify the Health
value, but code outside our class will no longer have access to it.
Now, the public interface is very simple and protects users of our class from sneaking past our intended behavior
Monster Goblin;
Goblin.TakeDamage(50); // This is allowed
Goblin.Health -= 50; // This isn't
Test your Knowledge
Accessing Class Members
In the following example, what is the first line in the code that will cause an error?
1class Weapon {
2public:
3 int Damage{50};
4};
5
6int main(){
7 Weapon IronSword;
8 IronSword.Damage;
9 IronSword.Damage += 30;
10}
What is the first line in the code below that will cause an error?
1class Weapon {
2private:
3 int Damage{50};
4};
5
6int main(){
7 Weapon IronSword;
8 IronSword.Damage;
9 IronSword.Damage += 30;
10}
Getters
After refactoring our previous class to move Health
to the private
section, we are now successfully implementing our class invariant. The Health
of a Monster
can never be negative.
But we've indirectly added more restrictions. Code outside our class can no longer tell how much Health
our monsters have:
#include <iostream>
using namespace std;
class Monster {
public:
void TakeDamage(int Damage){/*...*/};
private:
int Health{150};
};
int main(){
Monster Goblin;
cout << "Health: " << Goblin.Health;
}
error: 'Monster::Health': cannot access
private member declared in class 'Monster'
The typical approach for allowing external code access to our private members is to provide a simple function within the public
part of our class.
A function like this is sometimes called a getter. Some programming languages offer a dedicated syntax for this, but C++ keeps it simple. We just create a function in the normal way:
#include <iostream>
using namespace std;
class Monster {
public:
int GetHealth() { return Health; }
void TakeDamage(int Damage){/*...*/};
private:
int Health{150};
};
int main(){
Monster Goblin;
cout << "Health: " << Goblin.GetHealth();
}
Health: 150
This allows outside code to see the current Health
using the public GetHealth()
function. But, they cannot change the Health
, because the variable itself is still private.
Test your Knowledge
Adding Getters
Line 7 in the below code example is an error, as Damage
is a private member. How should we modify our class to allow line 7 to read the weapon's Damage
, but not change it?
1class Weapon {
2private:
3 int Damage { 50 };
4}
5
6Weapon IronSword;
7IronSword.Damage;
Setters
Setters have a similar purpose to getters except, predictably, they are functions that allow the outside world to update variables on our object.
The difference between making a setter available and just making the underlying variable public is that, with a setter being a function, we can control the process.
We can provide a setter for our Health
variable, but in a way that maintains the invariant that the Health
will never be negative:
#include <iostream>
using namespace std;
class Monster {
public:
int GetHealth(){ return Health; }
void SetHealth(int IncomingHealth){
if (IncomingHealth < 0) {
Health = 0;
} else {
Health = IncomingHealth;
}
}
void TakeDamage(int Damage){/*...*/};
private:
int Health{150};
};
int main(){
Monster Goblin;
cout << "Health: " << Goblin.GetHealth();
Goblin.SetHealth(-50);
cout << "\nHealth: " << Goblin.GetHealth();
}
Health: 150
Health: 0
Summary
In this lesson, we explored the concept of encapsulation and its implementation in C++ using access modifiers. The key points include:
- Class Invariants: Establishing rules for class behavior that users can rely on. For instance, ensuring a
Monster
's health never goes negative. - Encapsulation in C++: The process of bundling data and functions within a class and controlling how they are accessed and modified.
- Public and Private Access Modifiers: Using
public
andprivate
keywords to control access to class members. Public members are accessible from outside the class, while private members are not. - Implementing Class Invariants: Demonstrated by the
TakeDamage
function in theMonster
class, ensuring health does not go negative. - Getters and Setters: Functions that allow controlled access to private class members. Getters return the value of a private member, while setters allow modifying it under certain conditions.
Constructors and Destructors
Learn about special functions we can add to our classes, control how our objects get created and destroyed.