Step 6 of 7
Type erasure
std::function<int(int)> can hold a plain function, a lambda with captures, or any object with a matching operator(). None of them inherit from anything, yet one type holds them all, and you can copy it like a value. That technique is type erasure: the wrapper forgets ("erases") the concrete type, but still knows how to call, copy and destroy what's inside.
Classic polymorphism asks every type to inherit from a base class. That's intrusive: you can't make int or a third-party class inherit from your Shape, and a std::vector<std::unique_ptr<Shape>> gives you pointers and heap allocations instead of values. Type erasure keeps the inheritance inside the wrapper:
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
template <typename T>
Shape(T value) : self_(std::make_unique<Model<T>>(std::move(value))) {}
Shape(const Shape& other) : self_(other.self_->clone()) {}
Shape& operator=(const Shape& other) {
self_ = other.self_->clone();
return *this;
}
Shape(Shape&&) noexcept = default;
Shape& operator=(Shape&&) noexcept = default;
double area() const { return self_->area(); }
private:
struct Concept { // the interface, hidden inside
virtual ~Concept() = default;
virtual double area() const = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
};
template <typename T>
struct Model : Concept { // one per concrete type, generated
T value;
explicit Model(T v) : value(std::move(v)) {}
double area() const override { return value.area(); }
std::unique_ptr<Concept> clone() const override { return std::make_unique<Model>(*this); }
};
std::unique_ptr<Concept> self_;
};
struct Circle { double r; double area() const { return 3.14159 * r * r; } }; // no base class
struct Square { double s; double area() const { return s * s; } };
int main() {
std::vector<Shape> shapes{Circle{1}, Square{2}, Square{3}}; // values, not pointers
std::vector<Shape> copy = shapes; // deep copies
double total = 0;
for (const Shape& s : copy) total += s.area();
std::cout << total << "\n";
}
16.1416
How it works
Conceptis an ordinary abstract base class, but it's private toShape. Users never see it.Model<T>is a class template that wraps anyTand implementsConceptby forwarding toT's ownarea(). The templated constructor creates the rightModel<T>for whatever you pass, soCircleandSquareneed no base class, only a matchingarea()function.clone()makes copying work: copying aShapeasks the hidden model to copy the real object. SoShapebehaves like a value: you can copy it, store it in vectors directly, and pass it around by value.- The requirements are duck-typed: anything with
double area() constworks, including types written long beforeShapeexisted.
std::function, std::any and std::shared_ptr's deleter all work this way. Libraries add a "small buffer optimization" to store small objects inside the wrapper without a heap allocation, but the idea is the same.
Your turn: write Task, a type-erased wrapper for anything with int run() const. It needs the templated constructor, copying (with clone) and int run() const. The tests keep tasks of unrelated types in one std::vector<Task> and check that copies still work after the originals are gone.
Previous: RAII wrappers for C libraries Next: Challenge: a table-driven state machine