Phase 3 · Generic Programming & the Standard LibraryModule 17~50 min read

Lambdas, Callables & Functional Tools

Pass and compose behavior with lambdas, function objects, captures, std::function, and invocation utilities.

What you'll learn

Algorithms become flexible when behavior is an argument. Lambdas create small function objects at the point of use; callable wrappers and invocation utilities let behavior cross API boundaries. The central design question is not syntax—it is state and lifetime.

By the end, you'll be able to:

  • Write lambdas with parameters, captures, return types, and constraints
  • Choose value, reference, initialized, and move captures safely
  • Compare lambdas, function objects, function pointers, and std::function
  • Invoke callables generically while protecting captured lifetimes

Lambda expressions

A lambda expression constructs an unnamed function object. Its capture list holds state, parameters describe each call, and the body implements behavior. Return type is normally deduced; add -> Type when branches need an explicit common contract.

PartExampleRole
Capture[limit]State stored in the closure
Parameters(int value)Inputs for each call
SpecifiermutableAllows mutation of value-captured state
Return-> boolExplicit result type
Body{ return value < limit; }Executed behavior
sort_names.cpp
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> names{"Grace", "Ada", "Bjarne"};
    std::ranges::sort(names, [](const std::string& left, const std::string& right) {
        return left.size() < right.size();
    });

    for (const auto& name : names) std::cout << name << ' ';
}

Captures and ownership

Value capture stores a copy made when the lambda is created. Reference capture stores an alias and requires the original object to remain alive. Initialized capture can rename, transform, or move a value into the closure, making it the clearest form for owned state.

captures.cpp
#include <iostream>
#include <memory>

int main() {
    int threshold{10};
    auto above = [threshold](int value) { return value > threshold; };

    auto resource{std::make_unique<int>(42)};
    auto consume = [owned = std::move(resource)] {
        std::cout << *owned << '\n';
    };

    std::cout << std::boolalpha << above(12) << '\n';
    consume();
}

Watch out

A lambda returned, stored, queued, or run asynchronously must not retain references to local objects that will be destroyed first. Capture owned values or use an explicit lifetime-managed handle.

Generic and mutable lambdas

An auto parameter makes the closure's call operator a template. Constraints can narrow the accepted arguments. By default, a lambda's call operator is const; themutable specifier permits changes to its own value-captured state without changing the original external object.

generic_counter.cpp
#include <concepts>
#include <iostream>

int main() {
    auto twice = []<std::integral T>(T value) { return value * 2; };
    auto next_id = [current = 100]() mutable { return ++current; };

    std::cout << twice(21) << '\n';
    std::cout << next_id() << ' ' << next_id() << '\n';
}

Note

Each lambda expression has a unique closure type. Even two visually identical lambda expressions do not have the same type.

Function objects and pointers

A named function object is a class with operator(), useful when behavior needs a documented reusable type. A function pointer stores an address to a compatible free or static function but carries no captured state. Lambdas with no captures can convert to a matching function pointer.

callables.cpp
#include <iostream>

struct Clamp {
    int low;
    int high;
    int operator()(int value) const {
        if (value < low) return low;
        if (value > high) return high;
        return value;
    }
};

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

int main() {
    Clamp percentage{0, 100};
    int (*operation)(int){square};
    std::cout << percentage(140) << ' ' << operation(6) << '\n';
}

std::function and type erasure

std::function<Signature> stores different copyable callables behind one runtime interface. That flexibility can allocate and adds indirect-call overhead, so templates are preferable in performance-sensitive generic code. Use std::functionwhen heterogeneous callables must share one stable stored type.

callbacks.cpp
#include <functional>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::function<void(std::string)>> handlers;
    handlers.emplace_back([](const std::string& event) {
        std::cout << "log: " << event << '\n';
    });
    handlers.emplace_back([count = 0](const std::string&) mutable {
        std::cout << "count: " << ++count << '\n';
    });

    for (auto& handler : handlers) handler("saved");
}

Tip

Before calling a default-constructed or cleared std::function, test it in a Boolean context; invoking an empty wrapper throws std::bad_function_call.

invoke, bind_front, and composition

std::invoke uniformly calls functions, function objects, and member pointers.std::bind_front binds leading arguments into a callable without the placeholder syntax of older bind. Simple lambdas often remain the clearest adapter because their capture and call behavior are visible.

invoke.cpp
#include <functional>
#include <iostream>
#include <string>

struct Greeter {
    void greet(const std::string& name) const {
        std::cout << "Hello, " << name << "!\n";
    }
};

int main() {
    Greeter greeter;
    std::invoke(&Greeter::greet, greeter, "Maya");

    auto greet_maya{std::bind_front(&Greeter::greet, &greeter, "Maya")};
    greet_maya();
}
  • Prefer a template parameter when the callable type can remain generic
  • Prefer a function pointer for stateless C-style callback boundaries
  • Prefer std::function when stored copyable callables need one runtime type
  • Prefer a lambda as a readable local adapter or stateful operation

Recap & quick check

Key takeaways

  • A lambda constructs an unnamed function object with explicit call behavior and optional state.
  • Value captures own copies; reference captures borrow and can dangle.
  • Generic lambdas template their call operator, while mutable permits changing captured copies.
  • std::function offers runtime type erasure with flexibility and a cost.
  • std::invoke unifies calling syntax, but lifetime remains the callback designer's responsibility.

Quick check

1. When is a value capture copied?

2. What does mutable permit?

3. Which wrapper stores heterogeneous copyable callables behind one signature?

4. What is the main risk of storing a reference-capturing callback?

Next: Module 18 — Exceptions, optional, variant & expected, where APIs represent failure and alternative results deliberately.