A leafless tree photographed against an overcast sky, branches splitting in fractal patterns. Photo · Wilhelm Gunkel / Unsplash
Inheritance turns one class into a tree of classes — base at the trunk, branches and twigs above. A useful tool, used carefully.

Sometimes you have a class — say, an Animal — and you want a more specific kind of it: Dog, Cat, Penguin. Each of these is an Animal, with everything an Animal has, plus a few extras. C++ lets you express this directly with inheritance: the new class gets all the data and methods of the old one for free, and can add or override what it needs.

Inheritance is genuinely useful for some problems, and genuinely overused for many. We'll cover both ends today. By the end you'll know how to use it, and — equally important — when not to.

The mechanics

class Shape {
public:
    Shape(double x, double y): x_(x), y_(y) {}
    double x() const { return x_; }
    double y() const { return y_; }
protected:
    double x_, y_;
};

class Circle : public Shape {
public:
    Circle(double x, double y, double r): Shape(x, y), r_(r) {}
    double area() const { return 3.14159 * r_ * r_; }
private:
    double r_;
};

Circle c(3, 4, 5);
c.x();      // 3 — inherited from Shape
c.area();   // 78.5 — Circle's own method

Three ideas in there:

Every Circle is, by structure, a Shape with extra fields stuck on the end. The methods are also inherited — every Circle can answer x() and y() calls without you doing anything.

"Is-a" — the only good reason to inherit

The single litmus test for whether inheritance is the right tool: ask yourself whether "every Derived really is a Base, in every reasonable sense". If yes, inheritance fits. If no, you're probably reaching for the wrong hammer.

The classic mistake is to inherit because two things "share some methods". That's code reuse, and it's a valid goal — but inheritance enforces the much stronger claim that every instance of the derived type is interchangeable with the base type wherever the base is expected. Violate that and the program will wrong in subtle, distant ways.

The Liskov Substitution Principle

Barbara Liskov, 1987, gave the precise statement: "Wherever code expects a Base, you can pass a Derived and the code must still behave correctly." If Bird::fly() is a method, and Penguin inherits from Bird but its fly() throws an exception, you've broken the principle — somewhere, code that thought it was safe to call .fly() on a Bird is going to crash on a Penguin.

The fix is usually to revisit the model. Maybe fly() shouldn't be on Bird. Maybe the right base is FlyingBird with Penguin inheriting from a different class. Models that don't model reality cleanly should be refactored, not patched.

Composition is usually the right answer

"Favour composition over inheritance" is one of the most quoted rules in object-oriented design, and for good reason. Inheritance creates a tight coupling between two classes — every public method of the base is now a public method of the derived. Composition (storing one class inside another) creates a loose coupling — you only expose what you choose to expose.

// composition — Car HAS-A Engine
class Car {
public:
    void start() { engine_.ignite(); }
private:
    Engine engine_;      // Car owns an Engine, doesn't expose it
};

Need a different engine type later? Replace the field. The Car's interface doesn't change. Need to share an engine across cars? Use a smart pointer. Composition is more flexible, more testable, and easier to reason about than inheritance for at-least 80% of relationships.

Once you've internalised that, inheritance becomes the special case it should always have been: the right tool for genuinely interchangeable variants of a single concept.

Multiple inheritance — and the diamond

C++ allows a class to inherit from more than one base. Python has it. Java does not. The argument has been going on since 1985.

class FlyingThing     { public: void fly();     };
class SwimmingThing   { public: void swim();    };
class FlyingFish : public FlyingThing, public SwimmingThing {};

Useful for mixins: little capability classes you can graft onto something. Painful when both bases inherit from a common ancestor (the famous "diamond problem", solved with virtual inheritance — a feature you'll meet maybe twice in your career and then mercifully forget).

Modern style: inherit at most one substantial class, plus zero or more pure-interface mixins. Or use composition. Multiple inheritance of two non-trivial classes almost always signals a design that wants to be untangled.

Why this matters for AI

Open PyTorch's source. Find nn::Module. Now look at how a typical neural-network layer is defined: class Linear : public Module, class Conv2d : public Module, class LayerNorm : public Module. Every layer in every PyTorch model on Earth inherits from Module, which gives it the standard methods: forward(), parameters(), train(), eval(), state_dict(). Inheritance is the spine of the framework.

That's the right use of inheritance: a clean common interface (Module), with many concrete implementations that genuinely are Modules in the Liskov sense. You can pass any of them to anywhere a Module is expected and the code works.

Inheritance is sharp. Use it where the "is-a" really fits, and reach for composition the other 80% of the time.

Try it yourself

What's next

Inheritance gives you a tree of classes. The magic happens when the same line of code does the right thing for whichever derived class is actually in front of it. That's polymorphism — and it's built on a single C++ keyword: virtual.

Week 22 is Shape-Shifting — polymorphism, virtual functions, and the runtime-dispatch trick that powers nn::Module::forward() and a thousand other architectures.

Photo credit

Photo free under the Unsplash license. Tree · Wilhelm Gunkel.