C++ std::function: Type Erasure Explained
In C++, std::function serves as a general-purpose
polymorphic wrapper capable of storing, copying, and invoking any
callable entity—such as free functions, lambdas, member function
pointers, and functors—that matches a specified signature. It
accomplishes this through a design idiom known as type erasure. By
decoupling the static interface defined by its template signature from
the concrete underlying type, std::function enables uniform
runtime handling of disparate callable types without requiring them to
inherit from a common base class.
The Challenge of Heterogeneous Callables
In C++, every callable construct can have a completely distinct type.
A standard function pointer void(*)(int), a stateless
lambda, a stateful lambda capturing multiple local variables, and a
class overloading operator() are mutually incompatible at
compile time. Even two lambdas with identical signatures and
implementations possess unique, anonymous types generated by the
compiler. Templates allow functions to accept any callable generically,
but templates require compile-time resolution and cannot be stored
uniformly inside a standard container, such as a
std::vector, without fixing the concrete type.
The Mechanics of Type Erasure
std::function bridges compile-time templates and runtime
polymorphism by encapsulating the concrete type behind a uniform
interface. Internally, this is achieved through a bridge pattern
composed of three main layers:
The Public Wrapper (
std::function<R(Args...)>): The outer class exposes standard copy, move, and invocation operations. It presents a unified external type defined solely by the return typeRand argument typesArgs....The Abstract Concept (Interface): An internal abstract base class declares pure virtual functions corresponding to the operations required of the callable. Typically, this includes an
invokemethod matching the target signature, along with virtual methods to clone, move, or destroy the stored object:struct CallableBase { virtual ~CallableBase() = default; virtual R invoke(Args... args) = 0; virtual CallableBase* clone() const = 0; };The Templated Model (Concrete Implementation): A templated derived class inherits from the abstract base class and stores the actual callable object of type
F. The template parameterFpreserves the concrete type locally within the implementation class:template <typename F> struct CallableModel final : CallableBase { F target; CallableModel(F f) : target(std::move(f)) {} R invoke(Args... args) override { return target(std::forward<Args>(args)...); } CallableBase* clone() const override { return new CallableModel<F>(target); } };
When a callable is passed to std::function, the
constructor deduces its concrete type F, instantiates
CallableModel<F>, and stores it as a pointer to
CallableBase. The outer std::function only
interacts with the pointer to CallableBase, completely
hiding (erasing) F from the outer type signature.
Invocation and Lifetime Management
When invoking operator() on std::function,
the call is forwarded directly to
CallableBase::invoke(...). This triggers a virtual dispatch
mechanism, resolving at runtime to
CallableModel<F>::invoke(...), which finally executes
the stored callable.
Lifetime operations follow the same pattern:
- Copying: Invoking the copy constructor of
std::functiondelegates to the internal virtualclone()method, duplicating the underlyingCallableModel<F>with its specific state. - Destruction: The virtual destructor on
CallableBaseensures that the concreteCallableModel<F>is properly destroyed, executing any destructors required by captured variables or members withinF.
Small Buffer Optimization (SBO)
Allocating memory on the heap for every callable instantiation causes
performance degradation due to dynamic allocation overhead and cache
misses. To mitigate this, standard library implementations of
std::function utilize Small Buffer Optimization (SBO).
The outer wrapper reserves a fixed-size internal memory buffer
(typically the size of three or four pointers). If the callable type
F satisfies size and alignment requirements and is
nothrow-movable, it is constructed directly inside this internal buffer
using placement new. Dynamic heap allocation is reserved
only for callables whose captured state exceeds the internal buffer.
Instead of an explicit virtual class hierarchy, many production implementations optimize SBO further by using a single control function pointer (a manager function) alongside an invocation function pointer. The manager function handles copying, moving, and destroying the in-place or dynamically allocated instance, eliminating full virtual table overhead while maintaining complete type erasure.