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

Minimal black and neon-green technical artwork showing multiple callable forms converging into one C++ callable wrapper and a single invocation path.

std::function is the standard C++ callable wrapper used when one variable or interface must store different callable types behind one stable function signature. A free function, function pointer, lambda, functor, or bound callable can all be invoked through the same wrapper when their arguments and return value are compatible with that signature.

This lesson follows OSC++.031: std::any, OSC++.030: std::variant, and OSC++.029: std::optional. Those lessons focused on value wrappers. std::function applies the same broader idea of type erasure to behavior: it hides the exact callable type while preserving how that callable can be invoked.

Lesson Navigator

C++ Weekly compares lambdas, std::function, and function pointers, which is the central design choice in this lesson.

What Counts as a Callable?

In C++, a callable is any object or expression that can be used with function-call syntax or otherwise invoked through the language’s invocation rules. A named function is callable. A function pointer is callable. A lambda creates a callable closure object. A class that overloads operator() becomes a function object, often called a functor. Member-function pointers can also participate when the target object is supplied.

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

auto multiply = [](int a, int b) {
    return a * b;
};

struct Subtract {
    int operator()(int a, int b) const {
        return a - b;
    }
};

These three callables have different concrete types, but all can satisfy the logical signature “takes two int values and returns an int.” That common contract is what std::function can capture.

Reading a std::function Signature

std::function is declared in the <functional> header. Its template argument is a function type. For example, std::function<int(int, int)> means: “store any compatible callable that can be invoked with two int arguments and produces a result usable as an int.”

#include <functional>

std::function<int(int, int)> operation;

The wrapper’s type stays the same even when the object assigned into it changes. That is the key abstraction. Code that receives operation does not need to know whether the stored callable came from a function pointer, lambda, or class object.

Free Functions and Function Pointers

A normal free function can initialize a compatible std::function directly. The wrapper stores enough information to call the target later through the requested signature.

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

std::function<int(int, int)> op = add;

int result = op(4, 5);  // 9

A raw function pointer can do this job with less machinery when only free functions or compatible noncapturing lambdas are needed. The reason to reach for std::function is not that it is always faster or simpler. Its advantage is that it can present one uniform owning wrapper over many callable types.

Lambdas

Every lambda expression creates a unique closure type. Two lambda expressions that look identical still have different concrete types. std::function is useful when several lambdas must be placed into the same variable, container, callback slot, or interface.

int offset = 10;

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

int answer = transform(5);  // 15

The lambda above captures offset by value. Its closure object therefore carries state. A plain function pointer cannot represent that captured state, but std::function can own the closure object and invoke it later.

Function Objects and Functors

A class becomes callable when it defines operator(). This pattern predates lambdas and remains useful when the callable needs named state, multiple member functions, custom construction, or a reusable type with explicit semantics.

struct Scale {
    int factor;

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

std::function<int(int)> transform = Scale{3};
int answer = transform(7);  // 21

The caller only sees std::function<int(int)>. The stored object’s concrete type, here Scale, is hidden behind the wrapper.

C++ Weekly builds a simplified std::function-style implementation to expose the type-erasure mechanism underneath the public interface.

Type Erasure Inside std::function

The important implementation idea is type erasure. The wrapper accepts a concrete callable type at construction time, then stores internal machinery capable of copying, destroying, and invoking that object without exposing the original type in the wrapper’s public type.

Conceptually, the wrapper remembers two things: the callable’s stored state and a way to invoke that state using the requested function signature. Real standard-library implementations are more sophisticated, but this mental model explains why unrelated callable types can be placed into the same std::function.

This flexibility has a cost. Direct calls, inline lambdas, and templates give the compiler more information about the concrete callable and often make inlining easier. std::function deliberately hides some of that information to gain runtime substitutability.

Empty State and std::bad_function_call

A default-constructed std::function is empty. You can test it in a Boolean context before invoking it. Calling an empty wrapper throws std::bad_function_call.

std::function<void()> task;

if (task) {
    task();
}

// Calling task() here without checking would throw
// std::bad_function_call.

For callback APIs, this empty state can naturally mean “no callback installed.” It is still good design to define that meaning explicitly so callers know whether absence is expected or represents an error.

Copyability and Move-Only Callables

Traditional std::function requires its stored callable to satisfy the copyability rules needed by the wrapper. A lambda that owns a std::unique_ptr by move, for example, becomes move-only and therefore cannot be placed directly into std::function under the classic model.

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

auto move_only = [p = std::move(ptr)]() {
    return *p;
};

// std::function<int()> f = std::move(move_only);
// Not valid when the wrapper requires a copyable target.

Modern C++ also provides std::move_only_function in C++23 for cases where a move-only type-erased callable wrapper is appropriate. That distinction matters in task systems, asynchronous work queues, ownership-sensitive callbacks, and code that deliberately transfers resources into a callable.

Performance and Allocation

std::function may add indirect-call overhead compared with a directly known callable. Implementations also commonly use a small-object optimization so sufficiently small callable objects can be stored inside the wrapper itself, avoiding a separate heap allocation. The exact internal buffer size is an implementation detail and should not be treated as portable behavior.

The practical rule is simple: use std::function when its interface flexibility solves a real design problem, then measure if the call path is performance-sensitive. For a hot inner loop where the callable type is known at compile time, a template parameter or direct lambda often gives the optimizer more room.

std::invoke and Generalized Invocation

std::invoke, introduced in C++17, provides a uniform way to invoke many callable forms, including ordinary callables, member-function pointers, and member-data pointers. It is a building block for generic libraries that want one invocation expression to cover multiple callable categories.

#include <functional>

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

int result = std::invoke(square, 6);  // 36

C++ Weekly explains std::invoke and why generalized invocation matters in C++17 generic code.

Callable Decision Table

ToolBest FitStateRuntime Type ErasureTypical Overhead
Direct functionKnown behaviorExternal/globalNoLowest
Function pointerFree functions, noncapturing lambdasNo captured object stateLimitedLow indirect call
Lambda / functorCompile-time known callableYesNoOften optimizable/inlinable
std::functionRuntime-substitutable copyable callbacksYesYesIndirect call; possible allocation
std::move_only_functionMove-only callbacks in C++23YesYesSimilar abstraction cost
Template parameterMaximum compile-time specializationYesNoOften minimal runtime overhead

A Practical Callback Example

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

class Sensor {
public:
    void set_handler(std::function<void(double)> handler) {
        handler_ = std::move(handler);
    }

    void publish(double value) {
        if (handler_) {
            handler_(value);
        }
    }

private:
    std::function<void(double)> handler_;
};

int main() {
    Sensor sensor;
    std::string label = "temperature";

    sensor.set_handler([label](double value) {
        std::cout << label << ": " << value << '\n';
    });

    sensor.publish(72.5);
}

This interface does not care what concrete callback type the caller supplies. The object only requires something compatible with void(double). That makes std::function especially useful at runtime-configurable boundaries such as event handlers, UI callbacks, schedulers, hardware abstractions, test hooks, and plugin interfaces.

Common Mistakes

  • Using std::function automatically when a template or direct lambda would be simpler.
  • Calling an empty wrapper without checking it first.
  • Capturing references in a lambda that outlive the referenced objects.
  • Assuming every callable fits inside the implementation’s small-object buffer.
  • Expecting classic std::function to store move-only callables.
  • Forgetting that type erasure can reduce opportunities for inlining.
  • Using a raw function pointer when captured state is actually required.

Hands-On Exercise

  1. Create std::function<int(int, int)> op.
  2. Assign a free add function and call op(4, 6).
  3. Replace it with a lambda that multiplies the two inputs.
  4. Create a Subtract functor and assign that object to the same wrapper.
  5. Print the result after every replacement.
  6. Create an empty std::function<void()> and verify its Boolean state before invocation.
  7. Add a capturing lambda that stores an offset and observe that the wrapper can preserve that state.
  8. Rewrite one stage using a template parameter instead of std::function and compare the interface.
  9. Use std::invoke on one of the callables.
  10. Explain which design you would choose for a hot inner loop and which you would choose for a runtime-configurable callback registry.

Knowledge Check

  1. Which header defines std::function?
  2. What does std::function<int(double)> describe?
  3. Why can a capturing lambda be stored in std::function but not represented by a plain function pointer?
  4. What is type erasure in this context?
  5. What happens when an empty std::function is invoked?
  6. Why can std::function be slower than a direct lambda call?
  7. What is small-object optimization?
  8. When would a template parameter be preferable?
  9. What problem does std::move_only_function address?
  10. What does std::invoke provide?

Knowledge Check Answers

  1. <functional>.
  2. A callable wrapper that accepts one double argument and returns a result usable as an int.
  3. A capturing lambda owns closure state; a raw function pointer only identifies executable function code and cannot carry that capture object.
  4. Hiding the concrete callable type behind a stable invocation interface.
  5. It throws std::bad_function_call.
  6. Type erasure generally introduces an indirect call and can reduce compile-time visibility for inlining; some targets may also require dynamic allocation.
  7. An implementation technique that stores sufficiently small callable objects inside the wrapper rather than allocating separate heap storage.
  8. When the callable type can stay known at compile time and maximum specialization or optimization is desirable.
  9. It provides a standardized type-erased wrapper that can own move-only callables.
  10. A uniform invocation mechanism for functions, function objects, member-function pointers, and related callable forms.

References

Primary technical references: cppreference — std::function, cppreference — std::invoke, and cppreference — std::move_only_function. The video references above come from the verified C++ Weekly channel.

Key Takeaway

std::function trades some compile-time visibility for a clean runtime-polymorphic callback interface. Use it when unrelated callable types genuinely need to cross the same runtime boundary. Keep direct calls, lambdas, or templates when the concrete callable type can remain known and the extra abstraction is unnecessary.

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