Type Erasure II — std::function
Editor’s note: This article has been revised to distinguish compile-time adapter generation, construction-time binding, and runtime dispatch. The expanded taxonomy appears in Polymorphism, Type Erasure, and std::function.
std::function<R(Args...)> is a hybrid abstraction. Templates generate
callable-specific adapters while the callable type is known; construction
stores a callable together with its matching adapters; invocation later uses a
uniform erased signature.
- Original implementation analysis
- The public callable contract
- The second template level sees the callable type
- Storage, manager, and invoker
- Three distinct phases
- Consumer-side and binding-side knowledge
- Relationship to virtual interfaces
- The hybrid classification
- References
Original implementation analysis
std::function Implementation:
Inherits _Function_base to get the storage, responsible for data, itself contains _M_invoker:
_M_functor: A union structure that stores actual callable, might be pointer or heap allocated callable objects. The_M_init_functorwill move__f, the callable to be stored in_M_functor_M_manager: A function pointer in_Function_handlerto create/destroy/… the_M_functor_M_invoker: A function pointer in_Function_handlerto call the callable&_My_handler::_M_invoke/&_My_handler::_M_manager: Static functions bound with_Functortype providing clone/destroy operations, which operates on the stored callable object. So the stored callable must be compatible with those static function pointers, which is done during construction.
Points to _Function_handler type that do the type erasure of the passed actual callable type, responsible for code
After construction, the binding is fixed to a specific _functor, aka user callable. Even though std::function’s type is only determined by callable signature, _Function_handler’s type is also determined by the actual callable type that is passed by user during construction.
The binding between data and code is done at construction phase at compile time. If we assign a std::function variable to another instance, the data and code must be both changed at the same time, which is done during run time (using the swap(...) member function). This data and code binding pattern happens for all methods of implementing type erasure:
- Virtual classes are bound to their vtable during compile time
- Statically generated template functions (or user written functions implementing type erasure) bind data and code at compile time
This data and code binding during compile time is at the core of how type erasure works, since only after the binding, type can be erased.

The original analysis uses compile-time binding for the relationship encoded
between _Functor and _Function_handler<_Functor>. The compiler instantiates
the matching _M_invoke and _M_manager functions and hardcodes their
addresses as the operations compatible with that callable type. Construction
then materializes that precompiled relationship in a particular runtime object
by storing the callable and those function pointers together. Assignment moves
or replaces the complete data-and-code bundle so it remains type-compatible.
The public callable contract
The outer template specialization fixes the interface visible to consumers:
std::function<int(int)> operation;
Code that invokes operation knows the argument and result contract. It does
not need to know whether the stored target is a free function, lambda, bound
member function, or callable class.
operation = [](int value) { return value + 1; };
operation = &freeFunction;
operation = CallableObject{};
These targets have different concrete C++ types but satisfy the same callable contract.
The second template level sees the callable type
std::function<R(Args...)> has a templated constructor and assignment
operation. At each binding site, the compiler knows the concrete Callable
type and can instantiate an adapter such as this conceptual function:
template <typename Callable>
int invoke(void* storage, int argument)
{
return std::invoke(
*static_cast<Callable*>(storage),
argument);
}
Every instantiated adapter has a uniform erased signature even though its body
recovers a different Callable. Implementations also require callable-specific
operations for destruction, copy, move, and storage management.
Templates therefore supply static polymorphism at the binding edge: they generate the type-specific code that the erased runtime object will later use.
Storage, manager, and invoker
A common implementation model contains:
- storage for the callable, either inline or through allocated storage;
- an invoker pointer compatible with
R(Args...); and - manager or lifecycle operations for the concrete callable type.
In libstdc++, concepts such as _Any_data, _M_manager, _M_invoker, and
_Function_handler<Signature, Callable> implement this division. These names
and the exact layout are implementation details; the C++ Standard specifies
behavior, not this particular operation-table representation.
The essential invariant is that the stored object and all adapters agree on
the same concrete callable type. Copying, moving, assigning, or destroying a
std::function must update or use the storage and its associated operations as
one coherent unit.
Three distinct phases
The complete mechanism is easier to understand when compile time, construction, and invocation are kept separate.
Compile time: generate adapters
When a binding expression supplies Callable, the compiler instantiates or
selects callable-specific invocation and lifecycle code. The generated
functions are ordinary machine code in the program or one of its libraries.
Construction or assignment: bind runtime state
Construction stores the callable and records the matching invoker and lifecycle
operations in the std::function object. This is runtime object initialization,
even though the adapter code was generated earlier.
Assignment can replace the target. The object is not permanently bound after its first construction:
std::function<int(int)> operation =
[](int value) { return value + 1; };
operation =
[](int value) { return value * 2; };
The second assignment replaces both the callable state and the operations that know how to invoke and manage that state.
Runtime invocation: follow the stored invoker
operator() does not perform overload resolution among callable candidates.
Construction or assignment has already selected the adapter. Invocation passes
the erased storage and arguments through the stored invoker, which recovers the
callable type internally and executes it.
Compile time:
Callable is known
-> generate Callable-specific invoke and lifecycle adapters
Construction or assignment:
store Callable
-> store the matching adapter pointers
Runtime invocation:
operator()(arguments)
-> indirect call through the stored invoker
-> concrete Callable executes
Consumer-side and binding-side knowledge
The translation unit constructing or assigning the wrapper must know the
concrete callable type. A translation unit that only receives and invokes a
std::function<R(Args...)> needs to know only the erased signature.
binding side
knows Callable
-> constructs std::function<R(Args...)>
consumer side
knows only R(Args...)
-> invokes the stored callable indirectly
Concrete callable knowledge has not disappeared from the whole program. Type erasure confines that knowledge to the adapter-generation and binding site so the consumer can remain type-independent.
Relationship to virtual interfaces
This series classifies virtual interfaces operationally as type erasure. A
derived-object construction records a vptr associated with the derived type;
later calls through Base& load a vtable slot and invoke the override. The
compiler-generated vtable plays the role of an operation table.
std::function follows the same broad storage-plus-operations pattern without
requiring callable types to inherit from a public base. Its templated binding
adapter performs the work that a derived class and compiler-generated virtual
dispatch machinery perform in an inheritance hierarchy.
This terminology is broader than common usage, which often reserves type
erasure for wrappers such as std::function. The operational classification
is useful here because both designs hide a concrete implementation type behind
a uniform behavioral contract and preserve behavior through an indirect
operation table.
The hybrid classification
std::function combines three mechanisms:
- Static polymorphism: the templated binding operation generates callable-specific adapters.
- Type erasure: the resulting public value no longer exposes the callable
type; it exposes only
R(Args...). - Runtime polymorphism:
operator()dispatches through the invoker stored in that particular object.
The concise model is:
Templates generate the adapters, construction or assignment binds one callable to those adapters, and runtime invocation follows the stored invoker.
For type erasure of stored values rather than callables, see Type Erasure VII — std::any.