A minimalist black-and-white photograph of a single tree silhouetted against fog. Photo · Anas Ahmed / Unsplash
Abstraction is what survives when you remove the unnecessary. The shape that's left is the interface.

The other four pillars of OO — classes, encapsulation, inheritance, polymorphism — are mechanical. Abstraction is the one that sits in the head: what is the simplest, most useful description of the thing my code talks to?

Concretely, it means: program against an interface, not an implementation. Once you've done that, you can swap out the implementation later — for a faster one, a simpler one, a fake one for tests — and nothing else changes.

The interface as contract

An abstract base class in C++ — a class with at least one pure virtual method (= 0) — describes what something can do, without saying how. Any class that inherits and implements those methods becomes a usable concrete version.

class Logger {
public:
    virtual void log(const std::string& msg) = 0;
    virtual ~Logger() = default;
};

class ConsoleLogger : public Logger {
public:
    void log(const std::string& msg) override {
        std::cout << "[LOG] " << msg << "\n";
    }
};

class FileLogger : public Logger {
public:
    void log(const std::string& msg) override {
        /* write to file */
    }
};

Logger is the abstraction. ConsoleLogger and FileLogger are implementations. Code that uses logging takes a Logger& or Logger*not a specific subtype:

void authenticate(User& u, Logger& log) {
    log.log("authenticating " + u.name());
    // ... 
}

authenticate doesn't know — and doesn't want to know — whether logs go to the console, a file, or a remote server. It just wants something that can do log(msg). Swap in a NullLogger for tests; a JsonLogger for structured logging; a RemoteLogger for production. authenticate never changes.

The Dependency Inversion Principle

Robert Martin coined this in the early 2000s: "depend on abstractions, not on concretions." Translated: when class A needs to talk to class B, A shouldn't reach down and grab a specific kind of B — A should be handed something that satisfies the abstract interface and use it.

This is also the heart of dependency injection: you pass in the things a class depends on (typically as constructor arguments), rather than letting the class create them itself. The result is code that's easier to test, easier to reconfigure, and easier to refactor.

// bad — App is welded to ConsoleLogger
class App {
    ConsoleLogger log_;
};

// good — App talks to ANY Logger
class App {
public:
    App(Logger& log): log_(log) {}
private:
    Logger& log_;
};

The "good" version is wildly more flexible at very little cost. This pattern — pass in the dependency through the interface — is the structural skeleton of basically every framework you'll ever use, including AI ones.

Abstraction in the wild

Every successful piece of software has, at its core, a few well-chosen abstractions and a lot of replaceable implementations. The abstractions are the load-bearing decisions. Get them right and the system can evolve for decades. Get them wrong and the rewrites never end.

The cost of bad abstractions

Abstractions are leaky. The more you abstract, the more behavior the abstraction has to promise to capture — and the harder it gets to keep the promise. There's a wonderful Joel Spolsky quote: "all non-trivial abstractions, to some degree, are leaky." Pretending the network is reliable, that disks never fail, that floats are real numbers — these are convenient lies that occasionally bite. Good engineers know which abstractions leak and where.

Premature abstraction is also a real cost. Inventing five interfaces for hypothetical future needs slows you down today and may turn out to be wrong tomorrow. The pragmatic rule: start concrete; extract an abstraction the third time you need it. Two implementations isn't enough evidence; three usually is.

Why this matters for AI

Open the PyTorch source. Find at::Tensor. Notice it's a thin handle that points at a TensorImpl via std::shared_ptr. The TensorImpl owns a Storage. The Storage owns a DataPtr. The DataPtr uses an abstract Allocator. Each layer is an abstraction; each can be swapped (CPU allocator, CUDA allocator, MPS allocator, sparse-tensor variant) without disturbing the levels above.

That's also why "PyTorch on Apple Silicon" was a relatively contained engineering project — most of the framework didn't have to know that a new GPU backend existed. The abstractions held.

The right interface is the most valuable thing in a long-lived codebase.

Try it yourself

What's next

You now have classes, encapsulation, inheritance, polymorphism, abstraction — the whole OO arsenal. Time for object lifetimes: how they come into being, how they cease to exist, and why C++ nailed this better than any other mainstream language.

Week 24 is Constructors & Destructors.

Photo credit

Photo free under the Unsplash license. Silhouette · Anas Ahmed.

Quick check

1. What is an abstract base class?
  1. A class that compiles slowly
  2. A class with at least one pure virtual function — cannot be instantiated directly
  3. A class without member variables
Reveal Answer

Answer: B. Abstract classes describe a contract. You only instantiate concrete subclasses that fill in the pure virtuals.

2. What's the C++ idiom for an 'interface'?
  1. An XML file
  2. A class with only pure virtual functions and no member data
  3. A const reference
Reveal Answer

Answer: B. C++ doesn't have a separate interface keyword like Java. The pattern is just a pure abstract class.

3. Why design to abstract interfaces rather than concrete types?
  1. The concrete implementation can change without forcing every caller to change
  2. Abstract code runs faster
  3. The compiler optimises better
Reveal Answer

Answer: A. If 100 callers use Logger&, you can swap FileLogger for NetworkLogger without touching them.