Week 21 · Phase 3 — The Architect
Inheritance — base classes, derived classes, and the warnings that come with them.
Photo · Wilhelm Gunkel / Unsplash
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.
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:
: public Shape — the colon and access keyword declare that Circle publicly inherits from Shape. You'll see this hundreds of times in any C++ codebase. Other inheritance modes (protected, private) exist but are rare; public is the default in practice.: Shape(x, y), r_(r) — the constructor's initialiser list. Before Circle's body runs, the base class Shape must be constructed; we pass it the coordinates here. After that, Circle's own field r_ is initialised.protected: — visible to this class and its subclasses, but not to outside code. x_ and y_ in Shape are protected so that Circle (and any future Square, Polygon, etc.) can read them, but random code can't.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.
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.
Circle is a Shape. ✓ Inheritance fits.Dog is an Animal. ✓ Inheritance fits.Car is an Engine? No — a Car has an Engine. Inheritance is wrong; composition (storing an Engine field inside Car) is right.Stack is a Vector? Not really — a Stack restricts what you can do with a Vector. Use a member, or "private inheritance" (a niche feature).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.
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.
"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.
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.
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.
Shape / Circle example. Add a Square class. Make area() work for both.protected shows its purpose.Shape class that holds a "kind" enum and the right fields. Compare; either approach can be right depending on context.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 free under the Unsplash license. Tree · Wilhelm Gunkel.