Skip to content

Inheritance Cheatsheet

Inheritance is the strongest relationship in the language and the easiest to misuse. Use it when the is-a test passes out loud: a SavingsAccount is an Account.

Pairs with Module 6 — Inheritance


The test before you inherit

Question If no
Can I say "Derived is a Base" and mean it, with no exceptions? Use composition: the derived type has a base
Will every base operation make sense on the derived object? The hierarchy is wrong — split the base
Can a Base& hold the derived and every caller be correct? You are about to break Liskov substitution
Am I reusing code, with no relationship? That is not inheritance. Prefer a private member

The classic failure: a Square inheriting from Rectangle is true in geometry and false in code, because setting width and height independently is legal for one and corrupts the other.

If you want Use
Is-a, with substitutable behaviour public inheritance
Has-a, with a lifetime bound to the whole A value member, or unique_ptr
Has-a, but shared and independent A shared_ptr, or a non-owning reference
Behaviour that varies, with no relationship An interface + composition, or a std::function

Access modes, and what they actually change

Given class D : <mode> B, the mode controls how B's public and protected members appear inside D:

Mode B's public members in D B's protected members in D Who can convert D* to B*
public public protected Anybody
protected protected protected D and its derived classes
private private private Only D (and its friends)

Use public inheritance unless you have a specific reason — and if you do, write the reason down, because private inheritance (implementation reuse without an is-a relationship) is a signal that composition was the right answer.

Member access in the base Visible to derived class? Visible to outside callers?
public Yes Yes
protected Yes No
private No No

protected data members are usually a mistake. They let every derived class in the world depend on the representation, which is precisely what encapsulation was preventing. Keep data private and give derived classes small protected functions if they genuinely need a hook.


Construction and destruction with inheritance

Construct:  Base members → Base ctor body → Derived members → Derived ctor body
Destroy:    Derived dtor body → Derived members → Base ctor body → Base members

A derived constructor must initialise its base, either implicitly (the base's default constructor) or explicitly first in the initialiser list:

SavingsAccount::SavingsAccount(std::string owner, int opening, int rate)
    : Account(std::move(owner), opening),   // base first
      rate_(rate)
{
}

If the base has no default constructor and you forget this, the error is a no matching function for call to 'Account::Account()' pointing at the derived constructor.


Name hiding — the trap that looks like overriding

A derived member with the same name hides every base member of that name, regardless of signature. The base version is still there; it is just not found by name.

class Base { public: void f(int); void f(double); };
class Derived : public Base { public: void f(std::string); };

Derived d;
d.f(1);          // error: nothing converts 1 to std::string — Base::f is hidden
d.Base::f(1);    // explicit: hidden but reachable

Fix it with a using declaration, and say what you meant:

class Derived : public Base
{
public:
    using Base::f;                 // bring both base overloads back into scope
    void f(std::string);
};

-Woverloaded-virtual (part of -Wall) warns when a derived function hides a virtual base function. That warning is almost always a real bug:

error: 'virtual int Base::value() const' was hidden [-Werror=overloaded-virtual=]
note:   by 'int Derived::value()'

Inheriting constructors, final, and diamonds

class Derived : public Base
{
public:
    using Base::Base;      // inherit the base's constructors
};

class Leaf final : public Derived { };   // no further derivation
class Shape { public: virtual void draw() final; };   // no further overriding

Diamonds. If two bases share an ancestor, the class ends up with two copies of that ancestor, and names from it are ambiguous.

   Base
   /  \
 Left Right
   \  /
 Derived          ← two Base subobjects

virtual inheritance makes the shared ancestor appear once. It is the correct fix, and it is a signal that the design is doing something the language did not intend — reach for it rarely, and prefer composition when the two roles are genuinely different things.

Ambiguity message to expect: error: request for member 'x' is ambiguous followed by note: candidates are: ... Left::Base::x ... Right::Base::x. The note names both paths, which is how you confirm the diamond.


Checklist

  • [ ] Does the is-a sentence hold with no exceptions?
  • [ ] Is every base operation meaningful on the derived type?
  • [ ] Is the base's destructor virtual if anything deletes through base pointers?
  • [ ] Is there a using Base::name; wherever a name hides an overload set?
  • [ ] Are the base's data members private, not protected?
  • [ ] Did I consider composition, and can I say why inheritance was better here?

Related: Polymorphism · Class Design · SOLID · Module 6 — Inheritance