Skip to main content

Runtime Polymorphism Without Inheritance

Sean Parent’s technique for value-semantic polymorphism, and algebraic extensions.

The Problem

You want a container that holds different types:

std::vector<???> items;
items.push_back(42);
items.push_back(std::string("hello"));
items.push_back(MyCustomType{});

Traditional OOP says: make everything inherit from a base class. But that’s intrusive. You can’t add int to your hierarchy.

The Solution: Type Erasure

Store the interface in an abstract class, but wrap any type that provides the needed operations:

class any_regular {
    struct concept_t {
        virtual ~concept_t() = default;
        virtual std::unique_ptr<concept_t> clone() const = 0;
        virtual bool equals(concept_t const&) const = 0;
    };

    template<typename T>
    struct model_t : concept_t {
        T value;
        // Implement concept_t using T's operations
    };

    std::unique_ptr<concept_t> self_;
};

The key move: model_t<int>, model_t<string>, model_t<MyType> are all different types, but they all inherit from concept_t. The inheritance is pushed inside the wrapper. External types don’t need to know about it.

The Pattern

Three parts:

  1. concept_t: abstract base defining required operations.
  2. model_t: template that wraps any type T and implements concept_t.
  3. Wrapper class: holds unique_ptr<concept_t>, provides value semantics.

The wrapper looks like a value but can hold any type:

any_regular a = 42;           // Stores model_t<int>
any_regular b = "hello"s;     // Stores model_t<string>
any_regular c = a;            // Deep copy via clone()

Value Semantics

The key is clone(). Copying the wrapper deep-copies the held value:

any_regular(any_regular const& other)
    : self_(other.self_ ? other.self_->clone() : nullptr) {}

This gives us value semantics: copies are independent, assignment replaces content, no shared state surprises.

Beyond Regular: Algebraic Interfaces

Here’s where Stepanov’s insight applies. What if we type-erase algebraic structure?

any_ring: Erasing Ring Operations

class any_ring {
    struct concept_t {
        virtual std::unique_ptr<concept_t> clone() const = 0;
        virtual std::unique_ptr<concept_t> add(concept_t const&) const = 0;
        virtual std::unique_ptr<concept_t> multiply(concept_t const&) const = 0;
        virtual std::unique_ptr<concept_t> negate() const = 0;
        virtual std::unique_ptr<concept_t> zero() const = 0;
        virtual std::unique_ptr<concept_t> one() const = 0;
        virtual bool equals(concept_t const&) const = 0;
    };
    // ...
};

Now any_ring can hold integers, polynomials, matrices, or any ring, and you can do arithmetic on it at runtime:

any_ring a = 42;
any_ring b = polynomial{1, 2, 3};  // Different types!
// a + b would throw (type mismatch), but:
any_ring c = a + any_ring(10);     // Works: 52

any_vector_space: Linear Algebra Abstraction

A vector space over a field F has vector addition (v + w), scalar multiplication (alpha * v), and a zero vector (0).

template<typename Scalar>
class any_vector {
    struct concept_t {
        virtual std::unique_ptr<concept_t> clone() const = 0;
        virtual std::unique_ptr<concept_t> add(concept_t const&) const = 0;
        virtual std::unique_ptr<concept_t> scale(Scalar) const = 0;
        virtual std::unique_ptr<concept_t> zero() const = 0;
        virtual Scalar inner_product(concept_t const&) const = 0;
    };
    // ...
};

This lets you write algorithms that work on any vector space:

// Gram-Schmidt orthogonalization - works on any inner product space
template<typename Scalar>
std::vector<any_vector<Scalar>> gram_schmidt(
    std::vector<any_vector<Scalar>> const& basis
) {
    std::vector<any_vector<Scalar>> orthonormal;
    for (auto const& v : basis) {
        auto u = v;
        for (auto const& e : orthonormal) {
            // u = u - <u,e>*e
            u = u - e.scale(u.inner_product(e));
        }
        // Normalize u
        Scalar norm = std::sqrt(u.inner_product(u));
        orthonormal.push_back(u.scale(1.0 / norm));
    }
    return orthonormal;
}

The Stepanov Connection

This is Stepanov’s approach at the type level:

  1. Identify the algebraic structure (ring, vector space, etc.).
  2. Define operations axiomatically (not by inheritance).
  3. Let any type that provides the operations participate.

The concept/model pattern makes this work at runtime. It’s the same philosophy as C++20 concepts, but with runtime dispatch instead of compile-time.

Trade-offs

Costs: heap allocation, virtual dispatch (roughly 20ns overhead), and you can’t mix different concrete types in one operation.

Benefits: works with existing types (no inheritance required), value semantics, simple and generic algorithm code, and easy to add new types.

When to Use This

Plugin systems where concrete types aren’t known at compile time. Numerical libraries that support multiple matrix representations. Document systems with heterogeneous content. Undo/redo stacks with different action types.

Further Reading

  • Sean Parent, “Inheritance Is The Base Class of Evil” (GoingNative 2013)
  • Sean Parent, “Better Code: Runtime Polymorphism” (NDC 2017)
  • Stepanov & McJones, Elements of Programming, Chapter 1

Discussion