Understanding Type Erasure in C++
Type erasure is an advanced C++ design pattern that enables libraries
to provide uniform, non-templated dynamic interfaces while internally
supporting heterogeneous types without requiring an intrusive
inheritance hierarchy. This article examines what type erasure is,
breaks down the classic Concept-Model idiom used to implement it,
illustrates the pattern with a practical code example, and discusses how
standard library types like std::function and
std::any leverage this technique to provide flexible,
value-based dynamic polymorphism.
What Is Type Erasure?
Type erasure allows a programmer to manipulate objects of different
types through a single, concrete, non-templated class interface. In
traditional C++, runtime polymorphism requires types to inherit from a
common base class using virtual functions (intrusive polymorphism).
Conversely, compile-time polymorphism using templates requires
interfaces to expose generic parameters, which can lead to header bloat,
longer compilation times, and difficulty storing disparate objects in
homogeneous containers (like std::vector).
Type erasure bridges this gap. It captures compile-time type information, binds dynamic behavior internally via templates and function pointers (or virtual tables), and then conceals—or "erases"—the original type behind an ordinary, non-templated value wrapper.
The Concept-Model Idiom
The most common technique for implementing type erasure in modern C++ is the Concept-Model pattern, popularized by Sean Parent. This approach structures the implementation into three distinct layers:
- The Public Wrapper Class: A regular, non-templated class with value semantics (it can be copied, moved, and passed by value). This is the only type users interact with.
- The Abstract Base Class (The Concept): A private, pure virtual interface defined inside the wrapper. It declares virtual functions for every operation the wrapper needs to expose.
- The Templated Derived Class (The Model): A private, templated derived class that implements the Concept interface. It stores the concrete underlying object and forwards calls from the Concept interface to that underlying object.
Implementation Example
Below is a complete implementation of a non-intrusive
Drawable wrapper. It can hold any type that provides a
draw() member function, without requiring those types to
inherit from a shared base class:
#include <iostream>
#include <memory>
#include <utility>
#include <vector>
class Drawable {
public:
// Templated constructor accepts any type matching the requirements
template <typename T>
Drawable(T value)
: pimpl_(std::make_unique<Model<T>>(std::move(value))) {}
// Copy constructor (deep copy via clone)
Drawable(const Drawable& other)
: pimpl_(other.pimpl_ ? other.pimpl_->clone() : nullptr) {}
Drawable& operator=(const Drawable& other) {
Drawable temp(other);
pimpl_ = std::move(temp.pimpl_);
return *this;
}
Drawable(Drawable&&) noexcept = default;
Drawable& operator=(Drawable&&) noexcept = default;
// Non-templated public interface
void draw() const {
if (pimpl_) {
pimpl_->draw();
}
}
private:
// 1. Concept: Internal abstract base
struct Concept {
virtual ~Concept() = default;
virtual void draw() const = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
};
// 2. Model: Templated implementation
template <typename T>
struct Model final : Concept {
T data_;
explicit Model(T value) : data_(std::move(value)) {}
void draw() const override {
data_.draw();
}
std::unique_ptr<Concept> clone() const override {
return std::make_unique<Model<T>>(data_);
}
};
// 3. Storage
std::unique_ptr<Concept> pimpl_;
};Usage Without Inheritance
Because the polymorphism is non-intrusive, unrelated classes can be treated identically:
struct Circle {
void draw() const { std::cout << "Drawing Circle\n"; }
};
struct Square {
void draw() const { std::cout << "Drawing Square\n"; }
};
int main() {
std::vector<Drawable> shapes;
shapes.push_back(Circle{});
shapes.push_back(Square{});
for (const auto& shape : shapes) {
shape.draw();
}
}Benefits of Type Erasure
- Non-Intrusive Design: External classes, third-party libraries, and primitive types do not need to be refactored or modified to derive from a custom base class.
- Value Semantics: Unlike standard inheritance, which
requires holding objects by raw pointer (
Base*) or smart pointer (std::unique_ptr<Base>), type-erased wrappers behave like standard values with proper copy and move semantics. - ABI and Compilation Isolation: Templated code is kept entirely within the implementation details. Public interfaces remain non-templated, reducing compilation times and preventing implementation leakage across library boundaries.
- Heterogeneous Collections: Objects of different
types can be placed into standard containers like
std::vector<Drawable>without manual memory management.
Real-World Applications in the Standard Library
Type erasure is widely used across standard C++ facilities:
std::function<R(Args...)>: Erases the type of any callable object (lambdas, function pointers, member function pointers, or custom functors) that matches the specified signature.std::any: Stores any copy-constructible type and erases the static type entirely until explicit runtime queries (std::any_cast) are made.std::move_only_function(C++23): Similar tostd::function, but designed for move-only callables without requiring dynamic copy support.
By isolating templates to an internal instantiation step and presenting a stable, concrete type to the public interface, type erasure gives C++ developers the flexibility of dynamic polymorphism combined with the safety and performance of value-oriented programming.