C/C++ Arena

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:

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

Practice it