Before C++11, storing arbitrary callable objects required raw function pointers or custom polymorphic base classes. The introduction of std::function provided a type-erased wrapper capable of storing any callable, but it mandated CopyConstructible requirements and employed Small Buffer Optimization (SBO) to avoid heap allocation for small functors. As C++14 and C++17 popularized move-only types like std::unique_ptr, developers encountered the limitation that std::function could not store lambdas capturing unique resources. C++23 introduced std::move_only_function, which removes the copy requirement and supports move-only callables while maintaining SBO performance benefits.
std::function utilizes type erasure to hide the actual callable type behind a uniform interface. When the callable exceeds the internal buffer size (typically 16–32 bytes), the implementation allocates storage on the heap. However, the fundamental constraint is that std::function itself is copyable, requiring the type erasure mechanism to implement a "clone" operation via virtual dispatch. Consequently, the stored callable must be CopyConstructible, excluding move-only lambdas that capture std::unique_ptr or file handles. This forces developers to use std::shared_ptr (adding atomic overhead) or manual virtual inheritance (adding indirection).
std::move_only_function is a move-only wrapper that eliminates the CopyConstructible requirement. It achieves type erasure through a move-only vtable pattern, allowing it to store callables that can only be moved. Like std::function, it employs SBO, placing small functors directly in internal storage without heap allocation. This enables patterns like returning a lambda capturing a std::unique_ptr from a factory function, or storing exclusive ownership callbacks in containers without virtual dispatch overhead.
#include <functional> #include <memory> #include <iostream> // Simplified simulation of C++23 std::move_only_function template<typename Signature> class MoveOnlyFunc; template<typename Ret, typename... Args> class MoveOnlyFunc<Ret(Args...)> { struct Concept { virtual Ret call(Args... args) = 0; virtual ~Concept() = default; }; template<typename F> struct Model : Concept { F f; Model(F&& f) : f(std::move(f)) {} Ret call(Args... args) override { return f(args...); } }; std::unique_ptr<Concept> impl; public: template<typename F> MoveOnlyFunc(F&& f) : impl(std::make_unique<Model<F>>(std::forward<F>(f))) {} MoveOnlyFunc(MoveOnlyFunc&&) = default; MoveOnlyFunc& operator=(MoveOnlyFunc&&) = default; Ret operator()(Args... args) { return impl->call(args...); } }; int main() { auto ptr = std::make_unique<int>(42); // std::function would fail: capture of non-copyable type MoveOnlyFunc<void()> task = [p = std::move(ptr)] { std::cout << "Value: " << *p << " "; }; task(); // Output: Value: 42 }
Context: A high-frequency trading (HFT) platform processes market events through a thread-pool dispatching system. Each task encapsulates a network socket for sending responses, modeled as a std::unique_ptr<Socket> to ensure exclusive ownership and automatic cleanup.
Problem: The legacy dispatch queue used std::function<void()> for type erasure. When refactoring to modernize resource management by switching from raw pointers to std::unique_ptr, compilation failed with errors indicating the lambda was non-copyable. This blocked the migration because std::function cannot store move-only callables, forcing a reconsideration of the architecture.
Solutions considered:
1. Replacing unique_ptr with shared_ptr: Converting socket ownership to std::shared_ptr would satisfy std::function's copyability requirement.
Pros: Minimal code changes, standard std::function compatibility.
Cons: Atomic reference counting introduces microsecond-scale latency unacceptable in HFT. Semantically incorrect: sockets should not be shared between tasks; ownership must transfer exclusively.
2. Polymorphic task base class: Implementing an abstract Task interface with virtual execute() and storing std::unique_ptr<Task> in the queue.
Pros: Clean ownership semantics, no copyability requirements.
Cons: Virtual dispatch overhead (vtable indirection) adds nanoseconds to each call. Requires heap allocation for every task object, fragmenting memory in the hot path.
3. Custom move-only type erasure: Hand-rolling a template-based type erasure using std::aligned_storage and manual vtables.
Pros: Optimal performance, move-only support.
Cons: Fragile implementation requiring careful alignment handling and destructor management. Maintenance burden for template metaprogramming code.
4. Adopting C++23 std::move_only_function: Upgrading the compiler to support C++23 and replacing std::function with std::move_only_function.
Pros: Standardized solution with SBO (no heap for small closures), zero virtual dispatch overhead, native move-only support. Matches the exclusive ownership requirement perfectly.
Cons: Requires C++23 toolchain availability. Necessitates updating dependent APIs to accept the new type.
Chosen solution: Solution 4 was selected after confirming the trading firm's compilers supported C++23. The migration involved replacing std::function<void()> with std::move_only_function<void()> in the dispatch queue.
Result: The system successfully handled move-only socket resources. Benchmarks showed a 15% reduction in task dispatch latency compared to the shared_ptr approach, and zero heap allocations for small closures due to SBO. The codebase eliminated custom type erasure hacks, improving maintainability.
Why does std::function require the callable to be CopyConstructible even if the std::function object itself is never copied?
Candidates frequently assume that copyability is only checked when copying occurs. However, std::function is CopyConstructible by design. The type erasure mechanism must provide a "clone" operation in its virtual table to support copying the wrapper. If the stored callable lacks a copy constructor, this operation cannot be implemented, making the type incompatible at instantiation time. This is a compile-time constraint derived from the wrapper's type signature, not a runtime check. The standard requires the callable to model CopyConstructible to ensure the type erasure layer can satisfy std::function's own copy semantics.
How does Small Buffer Optimization (SBO) interact with exception safety during std::function moves?
Many candidates assume moving std::function is noexcept. While moving the wrapper itself is cheap, if the stored callable resides in the internal buffer (active SBO) and its move constructor is not noexcept, the std::function move constructor may propagate exceptions. This violates noexcept guarantees required by containers like std::vector for strong exception safety during reallocation. The standard does not guarantee noexcept moves for std::function unless the contained callable's move is noexcept and the implementation optimizes accordingly. This subtlety matters when storing std::function objects in containers that rely on noexcept move operations for performance.
Why can't std::function propagate reference qualifiers (&& or &) from the wrapped callable to its operator(), and how does std::move_only_function address this?
std::function's call operator is always const-qualified and treats the wrapper as an lvalue, regardless of the callable's reference qualifiers. This prevents invoking a callable that consumes resources (rvalue-qualified operator()) through the wrapper. std::move_only_function solves this by allowing the signature to specify reference qualifiers (e.g., std::move_only_function<void() &&>). It stores metadata or separate vtable entries to invoke the callable with the correct value category, enabling perfect forwarding of the wrapper's value state to the underlying callable. This allows the wrapped callable to distinguish between lvalue and rvalue invocations, crucial for move semantics in functional pipelines.