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.
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
virtualif 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