Controlling Inherited Member Access
Learn inheritance and access specifiers to create hierarchical relationships between classes.
What Are Inheritance Access Specifiers?
Every inherited member passes through two independent settings before anyone is allowed to touch it.
| Setting | Where you write it | What it decides |
|---|---|---|
| The member access specifier | Inside the base class, above the member | Who the base class is willing to let in |
| The inheritance access specifier | In the derived class header, after the colon | The strongest label the derived class is willing to republish |
Every example in this chapter so far has left the second setting at public, which is why it has been invisible. This lesson turns both settings independently, and the pay-off is a single question you can ask of any member: which of the two settings is more restrictive? That one is the one that wins.
To turn the settings you need a third access specifier that has not appeared yet, so we start there.
The Third Access Specifier: protected
public and private are already familiar. public members are open to everybody. private members are open to the class's own member functions and its friends, and to nobody else, and that "nobody else" includes derived classes.
That leaves a gap. A derived class is not the general public, but it is not the base class either. C++ fills the gap with protected: a protected member is reachable from the class's own member functions, from its friends, and from the member functions of any class derived from it, but not from outside code holding an object.
Consider a kiln in a pottery studio. The number of shelf slots is public information. The peak temperature is something a specialised kiln is entitled to retune. The hours logged on the heating elements belong to the kiln alone.
#include <iostream>
class Kiln
{
public:
int shelfSlots() const { return m_shelfSlots; }
bool needsService() const { return m_elementHours > 900; }
protected:
int m_peakCelsius{ 1180 };
private:
int m_shelfSlots{ 6 };
int m_elementHours{ 940 };
};
class RakuKiln : public Kiln
{
public:
RakuKiln()
{
m_peakCelsius = 1020; // protected in Kiln, so a subtype may write it
}
void describeLoad() const
{
std::cout << "raku firing: " << shelfSlots() << " slots at "
<< m_peakCelsius << " C" << '\n';
}
};
int main()
{
Kiln studioKiln{};
std::cout << "studio kiln slots: " << studioKiln.shelfSlots() << '\n';
std::cout << "service due: " << std::boolalpha << studioKiln.needsService() << '\n';
RakuKiln teaBowlKiln{};
teaBowlKiln.describeLoad();
return 0;
}
Output:
studio kiln slots: 6
service due: true
raku firing: 6 slots at 1020 C
Notice that needsService() reads m_elementHours without complaint. A class and its friends can always reach the class's own members, whatever specifier those members sit under. Access specifiers are aimed outward, at everybody else.
Where the Compiler Says No
The following program is deliberately broken. It attempts the three reaches that the previous example carefully avoided.
class Kiln
{
public:
int shelfSlots() const { return m_shelfSlots; }
protected:
int m_peakCelsius{ 1180 };
private:
int m_shelfSlots{ 6 };
int m_elementHours{ 940 };
};
class RakuKiln : public Kiln
{
public:
RakuKiln()
{
m_elementHours = 0; // private in Kiln: a subtype cannot see it
}
};
int main()
{
Kiln studioKiln{};
studioKiln.m_peakCelsius = 1200; // protected in Kiln, so main has no way in
RakuKiln teaBowlKiln{};
teaBowlKiln.m_peakCelsius = 1050; // still protected when reached via RakuKiln
return 0;
}
The compiler rejects all three:
s.cpp: In constructor 'RakuKiln::RakuKiln()':
s.cpp:19:9: error: 'int Kiln::m_elementHours' is private within this context
19 | m_elementHours = 0; // private in Kiln: a subtype cannot see it
| ^~~~~~~~~~~~~~
s.cpp:11:9: note: declared private here
11 | int m_elementHours{ 940 };
| ^~~~~~~~~~~~~~
s.cpp: In function 'int main()':
s.cpp:26:16: error: 'int Kiln::m_peakCelsius' is protected within this context
26 | studioKiln.m_peakCelsius = 1200; // protected in Kiln, so main has no way in
| ^~~~~~~~~~~~~
s.cpp:7:9: note: declared protected here
7 | int m_peakCelsius{ 1180 };
| ^~~~~~~~~~~~~
s.cpp:29:17: error: 'int Kiln::m_peakCelsius' is protected within this context
29 | teaBowlKiln.m_peakCelsius = 1050; // still protected when reached via RakuKiln
| ^~~~~~~~~~~~~
s.cpp:7:9: note: declared protected here
7 | int m_peakCelsius{ 1180 };
| ^~~~~~~~~~~~~
The first error is the one worth memorising. A private base member is invisible to derived classes, and no inheritance specifier anywhere can change that. Privacy is the one wall inheritance never climbs.
A derived class can reach base members declared
public or protected. It can never reach base members declared private, no matter how it inherits.
Choosing protected or private for a Base Member
Making a member protected buys convenience and sells insulation. Once derived classes read and write a data member directly, its name, its type, and the meaning of its value have all become part of a contract. Renaming m_peakCelsius, or switching it to Kelvin, now means editing the base class and hunting through every derived class in the codebase.
Keeping the member private leaves you free to change all of that behind a small set of accessor or mutator functions. The cost is that you have to write, test, and maintain those functions, and that the base class's interface grows to cover everything derived classes legitimately need.
That trade-off scales with how many derived classes exist and who owns them. If the hierarchy is yours, is small, and is unlikely to be extended by other teams, protected data is a defensible shortcut. If the base class is a library that strangers derive from, every protected member is a promise you cannot withdraw.
Start every base member off as private. Promote one to protected only when a planned derived class genuinely needs the direct access and wrapping the data in an interface would cost more than the coupling does.
The Second Dial: Choosing How to Inherit
The keyword between the colon and the base class name is the inheritance access specifier.
class Kiln
{
protected:
int m_peakCelsius{ 1180 };
};
class RakuKiln : public Kiln {}; // keyword written out: public
class SaltKiln : protected Kiln {}; // keyword written out: protected
class TestKiln : private Kiln {}; // keyword written out: private
class BisqueKiln : Kiln {}; // omitted on a class: private
struct TrialKiln : Kiln {}; // omitted on a struct: public
int main()
{
return 0;
}
Leaving the keyword out of a class definition gives you private inheritance, matching the way class members default to private. Leaving it out of a struct gives you public inheritance, matching the way struct members default to public. Relying on either default is a poor idea: readers should not have to remember which kind of definition they are looking at.
Omitting the inheritance keyword after the colon in a
class definition silently selects private inheritance, which is almost never what you meant. Always write the keyword.
Three member specifiers times three inheritance specifiers gives nine combinations. Before working through them, split the question in two, because only half of the nine actually varies.
What the Derived Class Itself Can Reach Never Changes
The inheritance specifier does not affect the derived class's own view of what it inherited. That view depends only on how the base declared the member:
| How the base declares the member | Can the derived class's member functions use it? |
|---|---|
public |
Yes, under every inheritance specifier |
protected |
Yes, under every inheritance specifier |
private |
No, under every inheritance specifier |
Private inheritance is the case that surprises people, so here it is running. SoakTimer inherits MinuteTicker privately and still calls its public functions freely from inside its own members:
#include <iostream>
class MinuteTicker
{
public:
void advance(int minutes) { m_elapsed += minutes; }
int elapsed() const { return m_elapsed; }
private:
int m_elapsed{};
};
class SoakTimer : private MinuteTicker
{
public:
void hold(int minutes) { advance(minutes); }
int held() const { return elapsed(); }
};
int main()
{
SoakTimer peakHold{};
peakHold.hold(25);
peakHold.hold(10);
std::cout << "held at peak for " << peakHold.held() << " minutes" << '\n';
return 0;
}
Output:
held at peak for 35 minutes
Think of the inheritance specifier as a setting on the derived class's shop window, not on its workbench. It never restricts what the derived class does with what it inherited.
What Everyone Else Sees: the Nine Combinations
What the inheritance specifier does control is the label the inherited member wears when viewed through the derived class. Two groups of people read that label: outside code holding a derived object, and any class derived further down the chain.
The rule is a ceiling. Each inheritance specifier caps how open an inherited member is allowed to look, and a member that was already more restrictive than the cap stays where it is.
| Declared in the base as | Seen through : public as |
Seen through : protected as |
Seen through : private as |
|---|---|---|---|
public |
public | protected | private |
protected |
protected | protected | private |
private |
inaccessible | inaccessible | inaccessible |
Reading the table by column:
: publicchanges nothing. A public base member is still public through the derived class, a protected one still protected. This is the ordinary case, and the one that models a genuine "is a" relationship.: protectedlowers the ceiling to protected. Public base members stop being callable from outside, but classes further down the chain keep their access.: privatelowers the ceiling to private. Everything the base offered is sealed inside the derived class: neither outside code nor further-derived classes can reach it.
The bottom row never moves, because a private base member was never accessible through the derived class in the first place.
Write
: public on your inheritance unless the design specifically calls for one of the other two. Both alternatives are rare enough that a reader will stop and wonder whether you meant it.
Although every example here uses data members and member functions, the same nine outcomes apply to every kind of member, including nested types and type aliases declared inside the base class.
Private Inheritance and the Closed Door
Return to SoakTimer. Inside the class, advance() was freely callable. From main, it is not, because private inheritance turned every inherited member private. The program below is deliberately broken.
class MinuteTicker
{
public:
void advance(int minutes) { m_elapsed += minutes; }
int elapsed() const { return m_elapsed; }
private:
int m_elapsed{};
};
class SoakTimer : private MinuteTicker
{
public:
void hold(int minutes) { advance(minutes); }
int held() const { return elapsed(); }
};
int main()
{
SoakTimer peakHold{};
peakHold.advance(25); // advance() is public in MinuteTicker, private in SoakTimer
return 0;
}
s.cpp: In function 'int main()':
s.cpp:21:21: error: 'void MinuteTicker::advance(int)' is inaccessible within this context
21 | peakHold.advance(25); // advance() is public in MinuteTicker, private in SoakTimer
| ~~~~~~~~~~~~~~~~^~~~
s.cpp:4:10: note: declared here
4 | void advance(int minutes) { m_elapsed += minutes; }
| ^~~~~~~
s.cpp:21:21: error: 'MinuteTicker' is not an accessible base of 'SoakTimer'
21 | peakHold.advance(25); // advance() is public in MinuteTicker, private in SoakTimer
| ~~~~~~~~~~~~~~~~^~~~
The second diagnostic names the real situation: the base is not accessible from outside at all, so the conversion from SoakTimer& to MinuteTicker& is off the table too.
That sealing is the entire point. Private inheritance suits the case where the derived class is implemented in terms of the base rather than being a kind of it. A soak timer is not a minute ticker, it merely uses one to keep count, and exposing advance() and elapsed() to callers would leak an implementation detail and let them corrupt the count. In most such cases composition, storing a MinuteTicker as a private data member, expresses the same idea more plainly, so private inheritance stays a rarity in practice.
When you catch yourself writing private inheritance, check whether a private data member of that type would do the job. Composition expresses "uses a" more plainly than inheritance does.
Protected Inheritance Keeps Access in the Family
Protected inheritance is the rarest of the three. It seals the base off from outside code exactly as private inheritance does, but keeps the door open for classes further down the chain.
Here SaltKiln inherits GlazeShelf protectedly, so stack() and loaded() become protected in SaltKiln. SodaKiln then inherits SaltKiln publicly, which leaves them protected, and so still usable from SodaKiln's own members:
#include <iostream>
class GlazeShelf
{
public:
void stack(int pots) { m_pots += pots; }
int loaded() const { return m_pots; }
private:
int m_pots{};
};
class SaltKiln : protected GlazeShelf
{
};
class SodaKiln : public SaltKiln
{
public:
void charge(int pots) { stack(pots); }
int onShelf() const { return loaded(); }
};
int main()
{
SodaKiln vapourKiln{};
vapourKiln.charge(14);
vapourKiln.charge(3);
std::cout << "shelf holds " << vapourKiln.onShelf() << " pots" << '\n';
return 0;
}
Output:
shelf holds 17 pots
Calling vapourKiln.stack(3) from main would fail, because stack() is protected in this hierarchy from SaltKiln downward. Had SaltKiln used private inheritance instead, SodaKiln would have lost access too, and charge() itself would not compile.
Summary
Two settings, not one. A member access specifier in the base class decides who the base lets in. An inheritance access specifier in the derived class header caps how open the inherited members look from outside. The more restrictive of the two wins.
The protected access specifier grants access to the class's own members, its friends, and its derived classes, while keeping outside code out. It exists specifically for inheritance.
Prefer private data in a base class. Protected data becomes part of your contract with every derived class, so changing it means changing them. Private data keeps you free to reshape the implementation behind a small interface. Reach for protected only when the derived classes are part of the plan and the interface would cost too much to build.
Derived classes never see private base members. No inheritance specifier changes this. Public and protected base members are reachable from derived member functions; private ones never are.
The inheritance specifier does not change the derived class's own access. A privately inheriting class can still call every public and protected member it inherited. Only the view from outside, and from further-derived classes, is affected.
The nine combinations reduce to a ceiling rule. Under : public, public stays public and protected stays protected. Under : protected, both become protected. Under : private, both become private. Private base members are inaccessible in all three cases.
Defaults are traps. A class with no inheritance keyword inherits privately; a struct inherits publicly. Write the keyword every time.
Public inheritance is the normal choice, and you should have a concrete reason before picking anything else. Private inheritance fits a class implemented in terms of a base it is not a kind of, though composition usually fits better. Protected inheritance seals the base from outside code while keeping it available further down the chain, and is almost never needed.
All members obey these rules, not just data members: member functions, nested types, and type aliases are governed by the same table.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Controlling Inherited Member Access - Quiz
Test your understanding of the lesson.
Practice Exercises
Access Specifier Exploration
Create a BankAccount base class with public balance getter, protected deposit method, and private account number. Create a SavingsAccount derived class that uses the protected method to add interest.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!