OSC++.032: std::function — Callable Wrappers, Type Erasure, Lambdas, Functors, and Callbacks

Minimal black and neon-green technical artwork showing several C++ callable forms converging into one generic callable wrapper and invoking a target.

std::function is a general-purpose C++ callable wrapper that lets one variable hold and invoke many different kinds of callable targets through one fixed function signature. A free function, lambda, functor, member-function pointer, or other compatible callable can be hidden behind the same interface when the program needs runtime flexibility instead of a compile-time template parameter.

This lesson follows OSC++.031: std::any, OSC++.030: std::variant, and OSC++.029: std::optional. Those lessons introduced vocabulary types and type erasure; std::function applies type erasure specifically to behavior.

Lesson Navigation

Mental Model: One Socket, Many Callables

Think of std::function<R(Args...)> as a socket with a fixed calling shape. The return type and parameter list are fixed, but the concrete object plugged into that socket can vary at runtime. The wrapper owns its target and exposes one common operation: call it.

CppCon’s “Back to Basics: Functions in C++” connects ordinary functions, function pointers, lambdas, and std::function.

The Signature Is the Contract

#include <functional>

std::function<int(int, int)> operation;
  • int before the parentheses is the return type.
  • int, int inside the parentheses are the parameter types.
  • Any compatible callable can be assigned if it can be invoked with two int arguments and produce a result compatible with int.
  • The wrapper type does not encode the concrete callable type.
Quick check: what does std::function<void(double)> mean?

It is a callable wrapper whose interface accepts one double argument and returns void.

What Can std::function Store?

Callable formExampleState?
Free functionint add(int, int)No object state
Function pointer&addNo object state
Lambda[scale](int x){ return x * scale; }Can capture state
FunctorObject with operator()Yes
Member-function pointer&Widget::runRequires an object argument in the wrapper signature
#include <functional>
#include <iostream>

int add(int a, int b) {
    return a + b;
}

struct Multiply {
    int factor{};

    int operator()(int value) const {
        return value * factor;
    }
};

int main() {
    std::function<int(int, int)> binary = add;
    std::cout << binary(2, 3) << '\n';

    int offset = 10;
    std::function<int(int)> lambda = [offset](int x) {
        return x + offset;
    };
    std::cout << lambda(5) << '\n';

    std::function<int(int)> functor = Multiply{4};
    std::cout << functor(6) << '\n';
}
Predict the output before opening

5, then 15, then 24.

CppCon’s lambda fundamentals explain captures and closure objects, which are commonly stored inside std::function.

Lambdas Are Function Objects

  • A lambda expression creates a closure object with a compiler-generated type.
  • A capturing lambda can carry state with it.
  • std::function lets code erase that unique closure type and store the lambda behind a named function signature.
  • That flexibility is especially useful at API boundaries where the concrete lambda type cannot conveniently appear in a non-template interface.
Arthur O’Dwyer’s CppCon session builds lambdas from first principles and connects them to functors and std::function.

Callbacks and Higher-Order Design

#include <functional>
#include <iostream>
#include <string>

void run_job(const std::string& name,
             const std::function<void(const std::string&)>& on_complete) {
    std::cout << "Running " << name << '\n';
    on_complete(name);
}

int main() {
    run_job("diagnostic", [](const std::string& job) {
        std::cout << job << " complete\n";
    });
}
  • The function receiving the callback does not need to know whether the caller passed a lambda, free function, or functor.
  • The callback signature documents exactly what the receiving function is allowed to call.
  • This pattern appears in event systems, asynchronous completion handlers, GUI actions, test hooks, command dispatch, networking, and hardware-control software.

Empty std::function Objects

std::function<void()> task;

if (task) {
    task();
}
  • A default-constructed std::function has no target.
  • operator bool() reports whether a target is present.
  • Calling an empty wrapper throws std::bad_function_call.
  • Checking the wrapper before invocation is the normal defensive pattern when emptiness is possible.
Why not just call it and catch the exception?

Because an empty callback can often be an expected state. Testing if (task) communicates that expectation directly and avoids using exceptions for routine branching.

How Type Erasure Makes This Work

The concrete callable type is hidden behind a uniform runtime interface. Internally, implementations typically keep enough machinery to invoke, copy, move, and destroy the stored callable without exposing its concrete type through the public wrapper. That is why several unrelated callable types can occupy the same std::function<R(Args...)> variable.

CppCon 2025 shows how type erasure powers abstractions such as std::function and examines the runtime tradeoffs.

Copyability Matters

  • std::function itself is copyable.
  • The target stored inside it must satisfy the required copyability constraints.
  • A lambda that owns a move-only object by value may therefore fail to fit std::function.
  • Modern C++ offers std::move_only_function in C++23 when a move-only callable wrapper is the correct abstraction.
#include <functional>
#include <memory>

int main() {
    auto ptr = std::make_unique<int>(42);

    auto move_only_lambda = [value = std::move(ptr)]() {
        return *value;
    };

    // std::function<int()> f = move_only_lambda;
    // This is not the right wrapper for a move-only target.
}

Performance: Flexibility Is Not Free

  • Invocation usually requires indirect dispatch through erased runtime machinery.
  • Some implementations use small-buffer optimization so small callables can live inside the wrapper itself.
  • Larger callables may require dynamic allocation.
  • The exact small-buffer size is an implementation detail.
  • Templates can often inline concrete callables more aggressively because the callable type stays visible to the compiler.
  • Performance-sensitive code should benchmark the actual workload instead of assuming the wrapper cost is either negligible or unacceptable.
Meeting C++ provides a deeper implementation-oriented examination of std::function, including its tradeoffs and internal design.

Choose the Wrapper That Matches Ownership

ToolCore ideaBest fit
std::functionOwning, copyable callable wrapperGeneral callbacks and runtime-polymorphic behavior
std::move_only_function C++23Owning, move-only wrapperCallbacks carrying move-only state
std::function_ref C++26Non-owning callable referenceShort-lived borrowed callables when lifetime is externally guaranteed
Template parameterCallable type stays concreteCompile-time generic code and maximum optimization opportunity
Function pointerPointer to a functionSimple stateless C-style callback interfaces
Decision helper

If the callback must own arbitrary copyable state and cross a non-template interface, start by considering std::function. If the callable is move-only, consider std::move_only_function. If ownership is not needed and C++26 is available, std::function_ref may be a better fit. If the callable type can stay known at compile time, a template is often simpler and faster.

Common Mistakes

  • Forgetting that an empty wrapper throws when called.
  • Using std::function in a hot inner loop without measuring the cost.
  • Capturing large objects by value and assuming the wrapper will never allocate.
  • Trying to store a move-only callable in std::function.
  • Choosing runtime type erasure when a simple template parameter would keep the design clearer.
  • Capturing references in a lambda and then allowing the referenced objects to die before the callback runs.

Hands-On Lab: Build a Command Dispatcher

#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, std::function<void()>> commands;

    int counter = 0;

    commands["status"] = [] {
        std::cout << "system ready\n";
    };

    commands["increment"] = [&counter] {
        ++counter;
        std::cout << "counter = " << counter << '\n';
    };

    commands["status"]();
    commands["increment"]();
    commands["increment"]();
}
  1. Compile with g++ -std=c++20 -Wall -Wextra -pedantic main.cpp -o app.
  2. Run the program and confirm the dispatcher invokes two different lambdas through one wrapper type.
  3. Add a free function called reset_system() and store it under the key reset.
  4. Add a functor that prints a diagnostic message and store it under diagnose.
  5. Before invoking a command, use find() so an unknown key does not accidentally create an empty wrapper.
  6. Add one callback that captures a string by value and another that captures an integer by reference. Explain the lifetime difference.

Challenge: Predict the Failure

std::unordered_map<std::string, std::function<void()>> commands;
commands["missing"]();
Reveal the failure

operator[] inserts a default-constructed empty std::function for the missing key. Invoking that empty wrapper throws std::bad_function_call. A lookup with find() or contains() avoids the accidental insertion and lets the program handle the missing command explicitly.

Knowledge Check

  1. What does the template argument in std::function<int(double)> describe?
  2. Can a capturing lambda be stored in std::function?
  3. What happens if an empty std::function is invoked?
  4. What operation checks whether a target is present?
  5. Why can std::function hide many different callable types behind one wrapper type?
  6. Why might a template parameter be faster?
  7. What C++23 wrapper is designed for move-only callable targets?
  8. What lifetime risk appears when a stored lambda captures local variables by reference?
1. Reveal answer

It describes the callable signature: return int and accept one double.

2. Reveal answer

Yes, if the closure object satisfies the wrapper’s requirements and its call operator matches the requested signature.

3. Reveal answer

It throws std::bad_function_call.

4. Reveal answer

Use the wrapper’s boolean state, commonly if (fn).

5. Reveal answer

Type erasure hides the concrete callable type behind one runtime-polymorphic interface.

6. Reveal answer

A template keeps the concrete callable type visible to the compiler, often allowing direct calls and more aggressive inlining.

7. Reveal answer

std::move_only_function.

8. Reveal answer

The callback may outlive the referenced object, creating a dangling reference. The captured object must remain alive for every invocation.

References

Primary technical references: cppreference — std::function and cppreference — function objects and wrappers. The standard library defines std::function in <functional> and has provided it since C++11.

Key Takeaway

std::function turns many concrete callable types into one stable runtime interface. Use it when the program genuinely benefits from owning and replacing arbitrary copyable callbacks behind one signature. Prefer simpler or more specialized alternatives when compile-time dispatch, move-only ownership, borrowed lifetime, or maximum performance better matches the problem.

Rotation next: Semiconductor Technician.

BitcoinVersus.Tech Editor’s Note:
We volunteer daily to ensure the credibility of the information on this platform is Verifiably True. If you would like to support to help further secure the integrity of our research initiatives, please donate using the QR code provided

Bitcoin donation QR code for 5,000 sats.

For best QR-code scanning results, please scan this donation code from a desktop or larger screen. Mobile screens may prevent camera recognition.

Leave a Reply