Shadowing Base Class Members
Hide or modify inherited members to control derived class interface.
What Is Hiding Inherited Functionality?
Public inheritance hands a derived class every accessible member of its base, keeping whatever access level the base chose. That default is usually right, but not always. A base class you did not write may expose a data member the derived class would rather keep sealed, or protect a helper the derived class wants to publish.
C++ gives the derived class three levers for reshaping the interface it inherits. All three change how a member is reached through the derived type, and none of them modifies the base class:
| Lever | Written in the derived class as | Result when the caller holds a derived object |
|---|---|---|
| Widen access | using Base::member; under a looser specifier |
The caller gains access it did not have |
| Narrow access | using Base::member; under a stricter specifier |
The caller loses access it used to have |
| Delete outright | returnType member(params) = delete; |
Any call is a compile error, whatever the access level |
That "through the derived type" qualifier is the whole lesson in one phrase. Every technique below is a rule the compiler applies to the type it can see at the call site, which is exactly why every technique below has a way around it.
Widening Access with a Using-Declaration
A using-declaration names an inherited member without parentheses and without a parameter list, and the access specifier it sits under becomes that member's access in the derived class.
Here an altimeter keeps its trace output protected, so only the class and its descendants can print it. A diagnostic build wants that output available to anyone:
#include <iostream>
class Altimeter
{
private:
int m_feet{};
public:
Altimeter(int feet)
: m_feet{ feet }
{
}
protected:
void traceAltitude() const { std::cout << "altitude " << m_feet << '\n'; }
};
class DiagnosticAltimeter : public Altimeter
{
public:
DiagnosticAltimeter(int feet)
: Altimeter{ feet }
{
}
using Altimeter::traceAltitude;
};
int main()
{
DiagnosticAltimeter probe{ 31500 };
probe.traceAltitude();
return 0;
}
altitude 31500
traceAltitude is still protected in Altimeter. An Altimeter object gives outside code no way to call it. Only the path through DiagnosticAltimeter was opened.
Write the member name alone:
using Altimeter::traceAltitude;, not using Altimeter::traceAltitude();. A using-declaration names a member, it does not call one, so parentheses and parameter lists are a syntax error here.
What You Cannot Widen
A derived class can only re-specify access for members it can already reach. Private base members are not reachable, so there is no lever to pull. The following does not compile, because m_feet is private in Altimeter:
#include <iostream>
class Altimeter
{
private:
int m_feet{};
public:
Altimeter(int feet)
: m_feet{ feet }
{
}
};
class DiagnosticAltimeter : public Altimeter
{
public:
DiagnosticAltimeter(int feet)
: Altimeter{ feet }
{
}
using Altimeter::m_feet;
};
int main()
{
DiagnosticAltimeter probe{ 31500 };
std::cout << probe.m_feet << '\n';
return 0;
}
s.cpp:23:22: error: 'int Altimeter::m_feet' is private within this context
23 | using Altimeter::m_feet;
| ^~~~~~
s.cpp:6:9: note: declared private here
6 | int m_feet{};
| ^~~~~~
Note where the compiler points: at the using-declaration on line 23, not at the attempted read on line 29. The declaration itself is rejected. Private stays private no matter how many layers of derivation you stack on top of it, which is the guarantee that makes private worth writing in the first place.
Narrowing Access to Hide a Member
Point the same lever the other way and a member that the base published disappears from the derived interface. This is the "hiding" the lesson is named for, and it is most useful when you inherit from a class whose encapsulation is weaker than you want.
LooseGauge exposes its reading as a raw public data member. SealedGauge pulls that member down to private and offers a controlled accessor instead. Reaching for the data member directly is now an error:
#include <iostream>
class LooseGauge
{
public:
int m_psi{};
};
class SealedGauge : public LooseGauge
{
private:
using LooseGauge::m_psi;
public:
SealedGauge(int psi)
: LooseGauge{ psi }
{
}
void publish() const { std::cout << "psi " << m_psi << '\n'; }
};
int main()
{
SealedGauge sealed{ 34 };
std::cout << sealed.m_psi << '\n';
return 0;
}
s.cpp: In function 'int main()':
s.cpp:26:25: error: 'int LooseGauge::m_psi' is inaccessible within this context
26 | std::cout << sealed.m_psi << '\n';
| ^~~~~
s.cpp:6:9: note: declared here
6 | int m_psi{};
| ^~~~~
SealedGauge itself is unaffected. Its own members still see m_psi, so publish() compiles and runs:
#include <iostream>
class LooseGauge
{
public:
int m_psi{};
};
class SealedGauge : public LooseGauge
{
private:
using LooseGauge::m_psi;
public:
SealedGauge(int psi)
: LooseGauge{ psi }
{
}
void publish() const { std::cout << "psi " << m_psi << '\n'; }
};
int main()
{
SealedGauge sealed{ 34 };
sealed.publish();
LooseGauge& raw{ sealed };
std::cout << "raw " << raw.m_psi << '\n';
return 0;
}
psi 34
raw 34
Look at the last two statements. Binding a LooseGauge& to the object and reading through that reference works fine, and prints the same value the narrowing was supposed to protect. That is not a compiler bug; it is the next section.
If you want every inherited member narrowed rather than one, do not write a using-declaration per member. Inherit privately instead:
class SealedGauge : private LooseGauge makes the whole base interface private in one stroke, and you can then use using-declarations to republish the few members you do want.
A Using-Declaration Moves the Whole Overload Set
A using-declaration names a member by name only, so when that name belongs to a set of overloads, every one of them moves together. There is no syntax for re-specifying access on a single overload while leaving its siblings alone, because there is nowhere to put the parameter list that would select one.
Both calls below fail, even though only one using-declaration was written:
#include <iostream>
class SignalBus
{
public:
int m_channel{};
int strength() const { return m_channel; }
int strength(int band) const { return m_channel + band; }
};
class ShieldedBus : public SignalBus
{
private:
using SignalBus::strength;
public:
ShieldedBus(int channel)
: SignalBus{ channel }
{
}
};
int main()
{
ShieldedBus bus{ 12 };
std::cout << bus.strength() << '\n';
std::cout << bus.strength(3) << '\n';
return 0;
}
s.cpp: In function 'int main()':
s.cpp:27:30: error: 'int SignalBus::strength() const' is inaccessible within this context
27 | std::cout << bus.strength() << '\n';
| ~~~~~~~~~~~~^~
s.cpp:8:9: note: declared here
8 | int strength() const { return m_channel; }
| ^~~~~~~~
s.cpp:28:30: error: 'int SignalBus::strength(int) const' is inaccessible within this context
28 | std::cout << bus.strength(3) << '\n';
| ~~~~~~~~~~~~^~~
s.cpp:9:9: note: declared here
9 | int strength(int band) const { return m_channel + band; }
| ^~~~~~~~
If you need one overload public and another hidden, the using-declaration is the wrong tool. Give the derived class its own function with a distinct name, or reconsider whether the base should be offering both overloads at all.
The Hiding Only Holds at Compile Time
Access control is decided entirely by the compiler, and it decides using the static type of the expression at the call site: the type the declaration says you have, not the type the object turns out to be at run time. Nothing about access survives into the generated program. There is no check, no flag, and no cost while the program runs.
That has a direct consequence. Narrowing access in SealedGauge changed what SealedGauge grants; it left LooseGauge exactly as permissive as it always was. Any expression whose static type is LooseGauge& therefore gets the base class's answer, which is how raw.m_psi compiled two sections ago. An upcast is not a loophole being exploited; it is the ordinary rule applied to a different static type.
Narrowing an inherited member's access is documentation with teeth for direct users of the derived class, and nothing more. It is not a security boundary and it is not a runtime guarantee. Any caller holding a base reference or base pointer sees the original access level.
The sharpest version of this appears with virtual functions, which the next chapter covers in depth. A base class publishes a virtual function; a derived class overrides it privately. Calling it on the derived object is rejected:
#include <iostream>
class Beacon
{
public:
virtual ~Beacon() = default;
virtual void emit() const { std::cout << "Beacon::emit" << '\n'; }
};
class QuietBeacon : public Beacon
{
private:
void emit() const override { std::cout << "QuietBeacon::emit" << '\n'; }
};
int main()
{
QuietBeacon quiet{};
quiet.emit();
return 0;
}
s.cpp: In function 'int main()':
s.cpp:20:15: error: 'virtual void QuietBeacon::emit() const' is private within this context
20 | quiet.emit();
| ~~~~~~~~~~^~
s.cpp:14:10: note: declared private here
14 | void emit() const override { std::cout << "QuietBeacon::emit" << '\n'; }
| ^~~~
Now route the same call through a Beacon&. The compiler checks access against Beacon, where emit() is public, so the call is allowed. Dispatch then happens at run time against the real object, which is a QuietBeacon:
#include <iostream>
class Beacon
{
public:
virtual ~Beacon() = default;
virtual void emit() const { std::cout << "Beacon::emit" << '\n'; }
};
class QuietBeacon : public Beacon
{
private:
void emit() const override { std::cout << "QuietBeacon::emit" << '\n'; }
};
int main()
{
QuietBeacon quiet{};
static_cast<Beacon&>(quiet).emit();
return 0;
}
QuietBeacon::emit
The private function ran. Access was checked once, against Beacon, and passed; run-time dispatch never consults access at all. This is worth internalising, because it means marking an override private communicates intent to readers without preventing anything.
Deleting an Inherited Function
Where a using-declaration re-specifies access, = delete removes the option entirely. Declare the inherited signature in the derived class and mark it deleted, and no call through a derived object compiles, regardless of which access section it sits in:
#include <iostream>
class Odometer
{
private:
int m_miles{};
public:
Odometer(int miles)
: m_miles{ miles }
{
}
int reading() const { return m_miles; }
};
class TamperProofOdometer : public Odometer
{
public:
TamperProofOdometer(int miles)
: Odometer{ miles }
{
}
int reading() const = delete;
};
int main()
{
TamperProofOdometer meter{ 91240 };
std::cout << meter.reading() << '\n';
return 0;
}
s.cpp: In function 'int main()':
s.cpp:31:31: error: use of deleted function 'int TamperProofOdometer::reading() const'
31 | std::cout << meter.reading() << '\n';
| ~~~~~~~~~~~~~^~
s.cpp:25:9: note: declared here
25 | int reading() const = delete;
| ^~~~~~~
The deletion applies to TamperProofOdometer::reading, and only to it. Odometer::reading is untouched and still reachable two ways: name it with a qualified call, or convert to the base type and let overload resolution find it there:
#include <iostream>
class Odometer
{
private:
int m_miles{};
public:
Odometer(int miles)
: m_miles{ miles }
{
}
int reading() const { return m_miles; }
};
class TamperProofOdometer : public Odometer
{
public:
TamperProofOdometer(int miles)
: Odometer{ miles }
{
}
int reading() const = delete;
};
int main()
{
TamperProofOdometer meter{ 91240 };
std::cout << meter.Odometer::reading() << '\n';
std::cout << static_cast<Odometer&>(meter).reading() << '\n';
return 0;
}
91240
91240
Cast to
Odometer&, not to Odometer. Casting to the value type copies the base portion of the object and calls the function on the copy, which costs a copy and quietly discards anything the derived part contributed.
When Hiding Means the Hierarchy Is Wrong
Every technique above is a repair, and repairs are worth noticing. The Liskov Substitution Principle says code written against a base class should keep working when handed a derived object. A function that takes Odometer& and calls reading() compiles happily and then receives a TamperProofOdometer whose whole design was to refuse that call. Nothing breaks at the call site, because nothing there was ever hidden. The promise made by the base class was simply not kept.
Hiding or deleting inherited members breaks substitutability. Any code holding a base reference bypasses your restriction without knowing it exists, so the invariant you meant to enforce is enforced in exactly the cases you were already watching and nowhere else.
Ask what the pattern is telling you before reaching for these tools. A derived class whose job is to take features away is not describing an is-a relationship; it is describing "like the base, but less", and inheritance has no way to express that honestly. Two alternatives model it better:
- Composition. Store the would-be base as a private member and expose only the operations you are willing to stand behind. No caller can upcast their way past the boundary, because there is no inheritance relationship to upcast through.
- Private inheritance. When you need the base's implementation but not its interface,
class Derived : private Basekeeps the reuse and removes the substitutability, since outside code cannot convert aDerivedto aBase.
Reserve access narrowing and
= delete for tightening a poorly designed base class you cannot edit, such as one from a third-party library. When the base class is yours, fix the base class or switch to composition instead.
Looking Forward
The theme running through this lesson, that the compiler decides using the static type while dispatch uses the dynamic one, is the same split the next chapter builds on deliberately. Virtual functions turn that gap into the point rather than the pitfall: you call through a base reference on purpose, and the derived behaviour runs. Reading the Beacon example again after that chapter is a good check on whether the distinction has landed.
Key Terminology
- Using-declaration: a statement of the form
using Base::member;in a derived class body, which brings the named base member into the derived class under the access specifier it appears beneath. - Widening access: placing a using-declaration under a looser access specifier than the base used, exposing a member to more callers through the derived type.
- Narrowing access (hiding): placing a using-declaration under a stricter access specifier, removing a member from the derived class's interface.
- Deleted function: a function declared with
= delete, so that naming it in a call is a compile error rather than a link error or a runtime failure. - Static type: the type an expression is declared to have, which is what all access checks are evaluated against.
- Liskov Substitution Principle: the design rule that a derived object must be usable anywhere its base type is expected, without the calling code needing to know the difference.
Summary
| Goal | Write in the derived class | Reachable through a base reference? |
|---|---|---|
| Publish a protected base member | using Base::member; under public: |
The base access still applies, so no |
| Hide a public base member | using Base::member; under private: |
Yes, the base access still applies |
| Hide every base member at once | Derive with private Base |
No, the conversion to Base& is unavailable |
| Forbid a call through the derived type | sig = delete; in the derived class |
Yes, via Base::member() or a Base& |
- A using-declaration re-specifies access for a member the derived class can already reach. Private base members cannot be widened, and the compiler rejects the using-declaration itself rather than the later call.
- The name is all a using-declaration carries, so an entire overload set changes access together. Selective re-specification of one overload is not expressible.
- Access is a compile-time rule evaluated against the static type of the expression. Narrowing in a derived class never alters what the base class grants, so an upcast restores the original access.
- Access is never rechecked at run time. A privately overridden virtual function called through a public base reference runs normally.
= deleteblocks calls through the derived type outright, whileobject.Base::member()andstatic_cast<Base&>(object).member()both still reach the base version.- Needing these tools often means inheritance was the wrong relationship. Composition or private inheritance expresses "reuses the base without promising to substitute for it" without leaving a hole for callers to fall through.
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.
Shadowing Base Class Members - Quiz
Test your understanding of the lesson.
Practice Exercises
Access Level Modification
Create a DataStore base class with a protected printData() method. Create a PublicDataStore derived class that uses a using-declaration to make printData() public. Then create a PrivateDataStore that hides a public getValue() method by making it private.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!