OSC++.032: std::function and Callable Objects — Lambdas, Function Pointers, Functors, Type Erasure, and Invocation

Minimal black and neon-green technical artwork showing multiple callable inputs converging into a single function wrapper and invocation path.

std::function gives C++ one stable, copyable wrapper for many different callable types. A free function, function pointer, lambda expression, functor, bind expression, or compatible member-function target can all be presented through the same signature such as std::function<int(int, int)>. This lesson follows OSC++.031: std::any, OSC++.030: std::variant, and OSC++.029: std::optional. The common theme is interface design: when should type information stay visible at compile time, and when is runtime type erasure the better engineering trade?

C++ Weekly with Jason Turner builds a simplified std::function implementation and exposes the mechanics behind the abstraction.

Lesson Navigation

Learning Objectives

  • Define what a callable object is in modern C++.
  • Declare std::function<R(Args...)> with the correct call signature.
  • Store free functions, function pointers, lambdas, and function objects.
  • Use operator bool to test whether a wrapper has a target.
  • Explain why invoking an empty wrapper throws std::bad_function_call.
  • Inspect stored targets with target_type() and target<T>().
  • Explain type erasure without confusing it with type conversion.
  • Recognize the copyability requirement imposed on stored targets.
  • Understand why type erasure can add indirection and sometimes allocation.
  • Choose among templates, raw function pointers, std::function, std::move_only_function, and std::function_ref.

The C++ Callable Model

Callable formExampleOwn state?Can std::function wrap it?
Free functionint add(int, int)NoYes
Function pointerint (*p)(int, int)NoYes
Captureless lambda[](int x){ return x * 2; }No captured stateYes
Capturing lambda[factor](int x){ return x * factor; }YesYes, if copy-constructible
FunctorMultiplier{3}UsuallyYes, if copy-constructible
Member-function target&Widget::runObject supplied at invocationYes

Your First std::function

#include <functional>
#include <iostream>

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

int main()
{
    std::function<int(int, int)> operation = add;
    std::cout << operation(7, 5) << '\n';
}
  • std::function is declared in <functional>.
  • The template argument describes the call signature, not the concrete stored type.
  • int(int, int) means “callable with two int arguments and producing an int result.”
  • The wrapper is invoked with normal function-call syntax: operation(7, 5).
g++ -std=c++20 -Wall -Wextra -pedantic main.cpp -o callable_demo
./callable_demo

Storing Lambdas and Functors

#include <functional>
#include <iostream>

struct Multiplier
{
    int factor;

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

int main()
{
    int offset = 10;

    std::function<int(int)> transform =
        [offset](int value)
        {
            return value + offset;
        };

    std::cout << transform(5) << '\n';

    transform = Multiplier{4};
    std::cout << transform(5) << '\n';
}
  • The first target is a capturing lambda containing its own copy of offset.
  • The second target is a function object whose operator() implements multiplication.
  • The concrete target type changes while the visible wrapper type remains std::function<int(int)>.
  • This ability is the practical value of type erasure: callers depend on the signature rather than the concrete callable class.

How Type Erasure Works

std::function performs type erasure: it preserves the behavior needed to copy, destroy, and invoke a target while hiding that target’s concrete C++ type behind one runtime wrapper. The wrapper does not convert every callable into a literal function pointer. Instead, it stores enough implementation machinery to dispatch a call through the declared signature. This lets unrelated callable types share one container or API boundary while retaining type-safe argument and return-value checking.

CppCon’s “Back to Basics: Type Erasure” demonstrates how wrappers such as std::function can be built from simpler mechanisms and then optimized.

Empty Wrappers and std::bad_function_call

#include <functional>
#include <iostream>

int main()
{
    std::function<void()> task;

    if (!task)
    {
        std::cout << "No target installed\n";
    }

    try
    {
        task();
    }
    catch (const std::bad_function_call& e)
    {
        std::cout << e.what() << '\n';
    }
}
  • A default-constructed std::function has no target.
  • if (task) checks whether a target is installed.
  • Invoking an empty std::function throws std::bad_function_call.
  • For normal callback APIs, checking the wrapper before invocation is usually clearer than intentionally relying on the exception.

Inspecting the Stored Target

#include <functional>
#include <iostream>
#include <typeinfo>

int square(int x)
{
    return x * x;
}

int main()
{
    std::function<int(int)> f = square;

    std::cout << f.target_type().name() << '\n';

    if (auto p = f.target<int(*)(int)>())
    {
        std::cout << (*p)(9) << '\n';
    }
}
  • target_type() reports the stored target’s runtime type.
  • target<T>() returns a pointer when the requested type exactly matches the stored target type.
  • A mismatch returns nullptr.
  • Target inspection is useful for diagnostics and specialized adapters, but ordinary code should usually invoke through the wrapper’s public signature.

Performance, Allocation, and Copyability

The flexibility of std::function is not free. Type erasure normally adds an indirect dispatch step, and a sufficiently large target may require dynamic storage; implementations commonly use a small-object optimization for some compact callables, but programs should not assume one universal inline-buffer size. The stored target must also satisfy the wrapper’s copyability requirements, which means a lambda that owns a move-only object cannot generally be placed into std::function. Performance-sensitive paths should benchmark the actual implementation rather than assigning one fixed cost to every call.

CppCon’s discussion of folly::Function explains why move-only callables expose an important limitation of classic std::function.

Modern Alternatives

ToolStandardOwnershipCopyable?Best fit
std::functionC++11OwningYesGeneral reusable callbacks with copyable targets
std::move_only_functionC++23OwningNoCallbacks that may capture move-only state
std::copyable_functionC++26OwningYesNewer copyable wrapper with richer qualifiers
std::function_refC++26Non-owningLightweight reference semanticsTemporary callable views when lifetime is guaranteed externally
Template parameterLanguage featureDepends on callerDepends on typeCompile-time generic code and maximum optimization opportunity
Raw function pointerLanguage featureNon-owningTrivialSimple stateless free-function callbacks

Decision Table

  • Need one runtime callback slot that can hold different copyable callables? Use std::function.
  • Need to capture a std::unique_ptr or another move-only resource? Consider std::move_only_function in C++23.
  • Need no ownership and the caller guarantees lifetime? Consider std::function_ref when C++26 is available, or a carefully designed alternative.
  • Know the callable type at compile time and want maximum inlining opportunity? Prefer a template parameter.
  • Only free functions with one exact signature are allowed? A raw function pointer may be sufficient.

Callback Registry Example

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

using Handler = std::function<void(const std::string&)>;

int main()
{
    std::unordered_map<std::string, Handler> handlers;

    handlers["log"] = [](const std::string& msg)
    {
        std::cout << "LOG: " << msg << '\n';
    };

    int warnings = 0;
    handlers["warn"] = [&warnings](const std::string& msg)
    {
        ++warnings;
        std::cout << "WARN: " << msg << '\n';
    };

    handlers.at("log")("System ready");
    handlers.at("warn")("Temperature high");

    std::cout << "Warnings: " << warnings << '\n';
}

Hands-On Exercises

  1. Create std::function<double(double, double)> and assign a normal function that multiplies two values.
  2. Replace the target with a lambda that divides the first argument by the second.
  3. Create a functor named Power that stores an exponent and invokes std::pow.
  4. Store Power{2} in std::function<double(double)>.
  5. Default-construct an empty callback and verify it with operator bool.
  6. Intentionally invoke the empty callback inside try/catch and identify std::bad_function_call.
  7. Create a capturing lambda that owns a std::unique_ptr<int>. Attempt to place it in std::function, observe the compile-time failure, then repeat with std::move_only_function under C++23.
  8. Build a small event registry using std::unordered_map<std::string, std::function<void()>>.
Checkpoint: Why does std::function need a signature?

The signature defines the stable public calling contract: return type, argument count, and argument types. Concrete callable types can differ internally as long as they are compatible with that contract.

Checkpoint: Why can a move-only lambda fail?

Classic std::function is copyable and requires the stored target to satisfy its copy-construction requirements. A lambda that directly owns a move-only resource may therefore be incompatible. C++23 std::move_only_function was designed for this class of use case.

Knowledge Check

  1. Which header defines std::function?
  2. Which C++ standard introduced it?
  3. What does the type std::function<bool(int)> describe?
  4. Name four categories of callable target that it can wrap.
  5. What happens when an empty std::function is invoked?
  6. How can code test whether a wrapper currently has a target?
  7. What does target<T>() return when T does not match?
  8. Why can a move-only capture be a problem?
  9. Which C++23 wrapper is designed for move-only callables?
  10. When can a template parameter be preferable to std::function?
Show knowledge-check answers
  1. <functional>.
  2. C++11.
  3. A callable interface that returns bool and accepts one int.
  4. Examples: free functions, function pointers, lambdas, functors, bind expressions, and compatible member targets.
  5. It throws std::bad_function_call.
  6. Use its contextual boolean conversion, such as if (callback).
  7. nullptr.
  8. The stored target for classic std::function must satisfy copyability requirements.
  9. std::move_only_function.
  10. When the callable type is known at compile time and static dispatch, inlining opportunity, or zero-overhead genericity matters.

References

Key Takeaway

  • std::function is an owning, copyable, type-erased callable wrapper.
  • Its template argument specifies the call signature rather than the concrete target type.
  • It is excellent for callback registries, event systems, task interfaces, and APIs that need one stable runtime callable type.
  • Templates remain stronger when the concrete callable can stay visible at compile time.
  • C++23 and C++26 add more specialized wrappers for move-only, richer copyable, and non-owning callable use cases.

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