Type Erasure

3 minute read

TL;DR

  • Both std::function and std::shared_ptr are implemented based on Type Erasure.
  • Type Erasure = A technique that wraps various unrelated types in a single interface to handle them uniformly.
  • std::function uses a Concept–Model structure to hide the calling interface (call operator).
  • std::shared_ptr uses the same pattern to hide the deleter (how the destructor is invoked).
  • Commonalities:
    • They store the “original type information” internally to guarantee correct behavior at runtime (invocation/destruction).
    • They internally cause virtual dispatch + heap allocation.

Overview

1) Why is std::function possible?

  • A single type like std::function<void(int)> can hold:
    • free functions
    • lambdas
    • functors
      which are completely different types of objects.
  • C++ is a strongly and statically typed language, yet this is possible because of Type Erasure.

2) Core Concepts of Type Erasure

Component Description
Concept (Interface) A pure virtual base class that defines only the functions that need to be called.
Model<T> A wrapper that wraps the type T and implements the Concept.
Container Holds a Concept* as a member and executes the actual function of Model<T> via virtual dispatch upon invocation.

In short:
Various types (T)Wrap with Model<T>Store as Concept*Use as a common interface
This flow represents the identity of std::function.


3) Why std::shared_ptr uses the same technique

  • std::shared_ptr<void> is possible, but
  • std::unique_ptr<void> cannot delete the object due to its default deleter.

Why?

  • unique_ptr<void> → The deleter is default_delete<void>, which makes it impossible to delete void*.
  • shared_ptr<void> → It stores the original object type T in the control block when created, allowing it to invoke T’s destructor when deleting.

That is, inside shared_ptr, there is Type Erasure structured as:

  • Control block + virtual deleter.

4) The Cost of std::function

  • Virtual call cost.
  • Heap allocation cost.
    • However, depending on the implementation, it may be stored on the stack via SBO (Small Buffer Optimization).

Example

1) std::function Type Erasure Structure (Simplified)

struct Concept {
    virtual void call(int) = 0;
    virtual ~Concept() = default;
};

template<typename T>
struct Model : Concept {
    T obj;
    Model(T o) : obj(o) {}
    void call(int x) override { obj(x); }
};

class Function {
    std::unique_ptr<Concept> ptr;
public:
    template<typename F>
    Function(F f) : ptr(std::make_unique<Model<F>>(f)) {}

    void operator()(int x) { ptr->call(x); }
};

With this, all of the following work:

Function f1 = printNum;
Function f2 = [](int x){ std::cout << x; };
Function f3 = PrintNumFunctor{};

2) shared_ptr Type Erasure Structure

struct ControlBlockBase {
    size_t refcount = 1;
    virtual void destroy() = 0;
    virtual ~ControlBlockBase() = default;
};

template<typename T>
struct ControlBlock : ControlBlockBase {
    T* ptr;
    ControlBlock(T* p) : ptr(p) {}
    void destroy() override { delete ptr; }
};

shared_ptr stores:

  • T* obj_ptr
  • ControlBlockBase* ctrl
template<typename T>
class SharedPtr {
    T* ptr;
    ControlBlockBase* ctrl;
public:
    template<typename U>
    SharedPtr(U* p)
        : ptr(p),
          ctrl(new ControlBlock<U>(p)) {}
};

That is, thanks to the ControlBlock<U> storing the type U,
shared_ptr<void> can accurately delete the original type.

Full Example Code


Takeaways

std::function

  • A container that abstracts multiple types into a “callable object”.
  • It hides type information using the Concept–Model–Container structure.
  • Overhead: virtual call + heap allocation.
  • However, its usability is powerful.

std::shared_ptr

  • shared_ptr<void> works safely because
    → it calls the deleter of the original object type when deleting the memory block.
  • This deleter is also a virtual-based Type Erasure structure.
  • The reason unique_ptr<void> cannot delete objects follows the same principle.

  • Type Erasure plays a core role in various parts of the C++ Standard Library.
  • A powerful pattern used to “handle heterogeneous types with a common interface.”
  • Widely used in std::function, std::shared_ptr, std::any, std::packaged_task, etc.

Reference

Unveiling C++ Type Erasure - From std::function to std::any - Sarthak Sehgal - C++Online 2025 CppNorth 2025, Sarthak Sehgal - Unveiling Type Erasure in C++: From std::function to std::any

Type erasure — Part I, Andrzej’s C++ blog Type erasure — Part II, Andrzej’s C++ blog Type erasure — Part III, Andrzej’s C++ blog Type erasure — Part IV, Andrzej’s C++ blog

Back to Basics: Type Erasure - Arthur O’Dwyer - CppCon 2019

Tags:

Categories:

Updated:

Leave a comment