Type erasure in C++: how std::function works
How std::function, std::any and friends hold unrelated types behind one value type, and how to write your own with a hidden interface and a template model.
std::function<int(int)> can hold a function pointer, a lambda with captures or any object with a matching operator(), although none of them share a base class. That's type erasure: the wrapper forgets the concrete type but still knows how to call, copy and destroy what's inside.
The standard recipe has three parts, all hidden inside the wrapper:
- a private abstract concept class with the operations you need, plus
clone()for copying; - a class template model that derives from it and forwards each call to the stored object;
- a templated constructor that creates the right model for whatever you pass, and a
std::unique_ptrto it.
The wrapper behaves like a value: you can copy it and store it in a std::vector directly, and any type with the right functions works, including types written long before the wrapper. std::function, std::any and std::shared_ptr's deleter all work this way; libraries add a small-buffer optimization to avoid the heap allocation for small objects.
Example
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
class Printable {
public:
template <typename T>
Printable(T value) : self_(std::make_unique<Model<T>>(std::move(value))) {}
Printable(const Printable& other) : self_(other.self_->clone()) {}
std::string text() const { return self_->text(); }
private:
struct Concept {
virtual ~Concept() = default;
virtual std::string text() const = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
};
template <typename T>
struct Model : Concept {
T value;
explicit Model(T v) : value(std::move(v)) {}
std::string text() const override { return value.text(); }
std::unique_ptr<Concept> clone() const override { return std::make_unique<Model>(*this); }
};
std::unique_ptr<Concept> self_;
};
struct Player { std::string name; std::string text() const { return "player " + name; } };
struct Score { int points; std::string text() const { return std::to_string(points) + " pts"; } };
int main() {
std::vector<Printable> feed{Player{"ada"}, Score{250}};
std::vector<Printable> copy = feed; // deep copies
for (const auto& p : copy) std::cout << p.text() << "\n";
return 0;
}
Output:
player ada
250 pts