GDKB

Inheritance

Intermediate20 Minutes readUpdated July 21, 20261 related concept

Inheritance lets a class (the **derived** or **child** class) reuse and extend another class's (the **base** or **parent** class) fields and methods, rather than duplicating them. `Goblin` and `Troll` might both inherit from `Enemy`, automatically getting `health`, `TakeDamage()`, and `Die()` without redeclaring any of it, while each adds or changes only what makes it distinct.

On this page

Inheritance

Summary

Inheritance lets a class (the derived or child class) reuse and extend another class's (the base or parent class) fields and methods, rather than duplicating them. Goblin and Troll might both inherit from Enemy, automatically getting health, TakeDamage(), and Die() without redeclaring any of it, while each adds or changes only what makes it distinct.

Mental Model

If a class is a blueprint, inheritance is a blueprint that starts from an existing one: "a Goblin house is a House, plus a reinforced door" — every room the base House blueprint specifies still exists in the Goblin variant, and the derived blueprint only needs to describe what's different, not redraw the whole plan from scratch.

Every Goblin object is also an Enemy object — this "is-a" relationship is exactly what inheritance models, and it's the reason Enemy-typed code (a variable, a parameter, a list) can hold and operate on a Goblin without knowing it's specifically a Goblin at all.

Core Explanation

  • Base class / derived class. class Goblin : Enemy (C#) declares Goblin as inheriting from Enemy — every public/protected field and method Enemy declares is available on Goblin objects too, without being rewritten.
  • protected is an access level between public and private: visible to the class itself and every class that derives from it, but not to unrelated outside code — the standard way a base class exposes something derived classes need without making it fully public.
  • Overriding. A derived class can redefine a base class method's behavior (override in C#, virtual/override in C++) — Goblin might override Die() to drop different loot than the base Enemy implementation. The base method must be declared virtual (or equivalent) to permit this; a non-virtual method cannot be overridden, only hidden, which is a different and usually-unintended thing.
  • Polymorphism is the direct payoff of overriding: code holding an Enemy reference that calls enemy.Die() runs whichever Die() the object's actual runtime type provides — Goblin's override if the object is a Goblin, the base implementation if it's a plain Enemy — decided at runtime based on the object's real type, not the type of the variable pointing at it.
  • Calling the base implementation. An override can still invoke the base class's version explicitly (base.Die() in C#, Super::Die() in Unreal C++) to extend rather than fully replace the inherited behavior — common when the derived class wants to add something in addition to what the base already does, not instead of it.
  • Single inheritance vs. multiple. C# and C++ classes support inheriting from one base class (C++ technically permits multiple base classes, but it introduces enough ambiguity — the "diamond problem" — that it is generally avoided in practice; Unreal's own class hierarchy is single-inheritance). This is exactly why Interfaces exist: to give a class more than one behavioral contract without needing multiple base classes.
  • Inheritance chains. Derivation can go more than one level deep (Boss : Goblin : Enemy) — a Boss object is simultaneously a Boss, a Goblin, and an Enemy, inheriting from the whole chain.

Syntax / Examples

C# (Unity)

text
public class Enemy
{
    public int Health = 100;
    public virtual void Die() { Debug.Log("Enemy defeated."); }
}

public class Goblin : Enemy
{
    public override void Die()
    {
        base.Die();               // still runs Enemy's shared logic
        DropLoot("gold-coin");     // plus Goblin-specific behavior
    }
}

// Polymorphism: same call, different behavior per actual object type
Enemy enemy = new Goblin();
enemy.Die(); // runs Goblin's override, even though `enemy` is typed as Enemy

C++ (Unreal)

text
UCLASS()
class AEnemy : public AActor
{
    GENERATED_BODY()
public:
    virtual void Die() { UE_LOG(LogTemp, Log, TEXT("Enemy defeated.")); }
};

UCLASS()
class AGoblin : public AEnemy
{
    GENERATED_BODY()
public:
    virtual void Die() override
    {
        Super::Die();     // Enemy's shared logic
        DropLoot();        // Goblin-specific addition
    }
};

Unreal Blueprint

Creating a Blueprint "Child Blueprint Class" of BP_Enemy establishes inheritance visually — the child inherits every variable and function from the parent. Overriding a parent function is done by right-clicking it in the child's function list and choosing "Override"; a Parent: Function node inside the override calls the base implementation, mirroring base.Die()/Super::Die().

Practical Example

A list typed as the base class can hold any derived type, and calling a shared method dispatches to each object's own override — the core reason inheritance pays off at scale:

csharp
List<Enemy> wave = new List<Enemy> { new Goblin(), new Troll(), new Enemy() };

foreach (Enemy enemy in wave)
{
    enemy.Die(); // Goblin's, Troll's, or the base Enemy's Die() — decided per object
}

The loop is written once against Enemy and never needs to change when a new enemy type is added — as long as the new type also derives from Enemy and (optionally) overrides Die(), it slots into the exact same loop with no modification to the loop itself.

Common Mistakes

  • Deep, rigid inheritance chains built for hypothetical future reuse — three or four levels deep, where most levels add little — make every change to a base class ripple through everything beneath it. Inheritance should model a genuine, stable "is-a" relationship, not be reached for by default.
  • Forgetting virtual on a base method intended to be overridden — in C++, calling a non-virtual method through a base-class pointer always runs the base implementation regardless of the object's actual type, silently defeating the polymorphism the derived override was supposed to provide.
  • Overriding without calling the base implementation when the base behavior was still needed — replacing Die() entirely instead of extending it can silently drop shared logic every other enemy type still relies on.
  • Inheriting purely for code reuse without a real "is-a" relationship — a PlayerInventory inheriting from EnemyInventory just to reuse a few fields, when a player is not conceptually a kind of enemy, is a sign the shared logic belongs in a separate reusable class instead (or an interface, see Interfaces), not in an inheritance relationship.
  • The fragile base class problem — changing a base class's behavior can silently break derived classes that depended on the old behavior, even if they never touch the base class's code directly.

Best Practices

  • Use inheritance only for a genuine, stable "is-a" relationship — ask "is a Goblin actually a kind of Enemy" (yes) versus "does Goblin merely want to reuse some of Enemy's code" (reach for composition or an interface instead).
  • Mark a base method virtual deliberately, only when derived classes are meant to be able to change its behavior.
  • Call the base implementation (base./Super::) when overriding, unless the derived behavior is genuinely meant to fully replace it.
  • Keep inheritance chains shallow; prefer composing smaller, focused classes over deep hierarchies built for anticipated reuse.

Exercises

  1. Model Goblin and Troll as classes deriving from Enemy, each overriding Die() differently, and demonstrate polymorphism with a loop over a list typed as Enemy.
  2. Explain why forgetting virtual on a base method in C++ silently breaks polymorphism, using a concrete before/after example.
  3. Identify a case where inheritance was used purely for code reuse without a genuine "is-a" relationship, and redesign it using composition instead.
  4. Write an override that calls its base implementation and explain what would be lost if that call were omitted.
  • Classes and Objects — inheritance relates two classes; the "is-a" relationship it creates is observable on the objects instantiated from them.
  • Interfaces — an alternative way to share a behavioral contract across unrelated classes, without inheriting implementation.

References

Related Concepts