C++ Inheritance Access Specifiers: Deep Dive
Core Concepts
Access specifiers in inheritance affect two main aspects:
1. Derived Class Access: How inherited members can be used within the derived class.
2. Client Code Access: How inherited members can be accessed through derived objects.
The three specifiers (public, protected, private) define different levels of encapsulation.
class Base {
public:
int x;
protected:
int y;
private:
int z;
};
class PublicDerived : public Base {
// x stays public, y stays protected, z is inaccessible
};
class ProtectedDerived : protected Base {
// x becomes protected, y stays protected, z is inaccessible
};
class PrivateDerived : private Base {
// x becomes private, y becomes private, z is inaccessible
};
Detailed Access Rules
Base Member | Inheritance Type | In Derived Class | Through Derived Object |
---|---|---|---|
public | public | public | public |
public | protected | protected | inaccessible |
public | private | private | inaccessible |
protected | public | protected | inaccessible |
protected | protected | protected | inaccessible |
protected | private | private | inaccessible |
private | any | inaccessible | inaccessible |
Defaults & Conversions
- Default inheritance access: If you omit the specifier, class Derived : Base
defaults to private inheritance; struct Derived : Base
defaults to public inheritance.
- Implicit upcasting: With public inheritance, clients can implicitly convert Derived*
/Derived&
to Base*
/Base&
. With protected or private inheritance, such conversions are not accessible to client code (they are only permitted within the derived class and, for protected, its further derived classes/friends).
struct S : Base {}; // defaults to public inheritance
class C : Base {}; // defaults to private inheritance
void f(Base*);
int main(){
PublicDerived pd; f(&pd); // OK: public upcast
// ProtectedDerived pr; f(&pr); // Error in client code: upcast not accessible
// PrivateDerived pv; f(&pv); // Error in client code: upcast not accessible
}
Practical Scenarios
Public Inheritance:
- Models 'is-a' relationships.
- Preserves the base class interface.
- Used in most inheritance cases.
Protected Inheritance:
- Rarely used in practice.
- Converts public base members to protected.
- Useful when derived classes need access but external clients should not.
Private Inheritance:
- Models 'implemented-in-terms-of' relationships.
- Completely hides the base class interface from clients.
- Often replaced by composition when possible.
// Public inheritance (interface)
class Circle : public Shape { /* ... */ };
// Private inheritance (implementation reuse)
class Stack : private LinkedList { /* ... */ };
Advanced Techniques
Using-declarations to re-expose: Bring hidden or redefined base names into the desired access section to change their accessibility in the derived interface.
Using Declarations: Bring hidden or redefined names into scope.
Friendship and Inheritance: Friend relationships are not inherited automatically.
class Base {
protected:
void helper();
};
class Derived : private Base {
public:
using Base::helper; // Re-expose helper as public via using-declaration
};
Design Guidelines
- Prefer public inheritance when modeling interface relationships.
- Use private inheritance only for implementation reuse.
- Avoid protected inheritance; consider private inheritance or composition instead.
- Document non-public inheritance relationships clearly.
- Use `using` declarations selectively to re-expose only the members you intend to.
- Do not alter access levels to bypass good design practices.