Skip to content
C++

Lambda Expressions and std::function

A lambda expression is a concise way to define an anonymous callable object right where you need it. Lambdas were added in C++11 and have been progressively enhanced through C++14, C++17, and C++20. Understanding what the compiler generates — and what that costs — is the key to using them well.

Why lambdas exist — the functor problem

Before C++11, passing a callable to an algorithm like std::sort or std::for_each required either a free function or a functor — a class with an operator(). Functors were powerful: they could hold state, unlike function pointers. But they had a critical limitation: you had to define them far from the call site, usually at file scope or class scope. Local functors were forbidden in C++98/03 — you could not instantiate a template with a local type.

// C++98: filter with a functor — defined elsewhere, used here
struct IsGreaterThan5 {
    bool operator()(int x) const { return x > 5; }
};

// C++11: same filter inline, right where it's used
auto it = std::find_if(v.begin(), v.end(), [](int x) { return x > 5; });

The C++11 committee's solution was elegant: the compiler itself would generate the functor. With some new syntax, you could write an anonymous functor inline, at the call site, and the compiler would expand it into a proper class with an operator(). That expansion is exactly what a lambda expression is.

Anatomy of a lambda expression

A lambda expression has up to six parts. Only the capture clause and the body are mandatory:

[captures] (parameters) specifiers -> return_type { body }
^           ^            ^              ^
|           |            |              optional: trailing return type
|           |            optional: mutable, constexpr, noexcept
|           optional: parameter list (required if specifiers are used)
capture clause — introduces the lambda
// Examples of increasing complexity:
[]{}                                         // simplest: no capture, no params, empty body
[](int a, int b) { return a + b; }          // two params, deduced return type (int)
[](int a, int b) -> double { return a + b; }// explicit return type
[x](int a) noexcept { return a + x; }       // captures x by value, noexcept
[&x](int a) mutable { ++x; return a; }      // captures x by ref, mutable body
[](auto x, auto y) { return x + y; }        // generic lambda (C++14)

The closure type — what the compiler actually generates

When the compiler sees a lambda expression, it generates a unique, unnamed class — the closure type. The lambda expression itself is a prvalue of that type, called the closure object. Because the name is unknown, you must use auto to hold it. Two lambdas with identical source code still have different closure types — each lambda gets its own unique class.

// The compiler transforms this lambda:
auto lam = [](int x) { std::cout << x << '
'; };

// ...into roughly this hidden functor:
struct __uniqueClosureType_42 {
    inline void operator()(int x) const {  // const by default
        std::cout << x << '
';
    }
};
auto lam = __uniqueClosureType_42{};

// Proof that two "identical" lambdas have different types:
auto a = [](int x) noexcept { return x * 2; };
auto b = [](int x) noexcept { return x * 2; };
// std::is_same_v<decltype(a), decltype(b)> == false

The call operator is const by default, meaning it cannot modify captured-by-value members. Specifiers like mutable and noexcept change that generated signature. In C++11/14, closure types had deleted default constructors and copy assignment operators. C++20 relaxed this for stateless (non-capturing) lambdas, which became default-constructible and assignable.

Captures — how lambdas access outer variables

Capturing a variable from the enclosing scope creates a member variable in the closure type. Captured-by-value variables become data members initialized at the point where the lambda is defined — not where it is called. Captured-by-reference variables become reference members and reflect the value at call time, but the closure does not extend the lifetime of the referenced variable.

Capture clauseWhat it does
[]No capture — accesses nothing from outer scope
[=]Capture all automatic-storage variables by value (copy)
[&]Capture all automatic-storage variables by reference
[x]Capture x by value explicitly
[&x]Capture x by reference explicitly
[x, &y]x by value, y by reference
[=, &x]All by value, except x by reference
[&, x]All by reference, except x by value
[this]Capture this pointer (C++11) — access member variables via pointer
[*this]Capture entire object by value (C++17) — safe for async/async member lambdas
[z = expr]Init capture (C++14) — create new member z, initialize to expr
[p = std::move(p)]Move a unique_ptr into the closure (C++14)
int x = 10, y = 20;

// [=]: captures snapshots — x and y are frozen at lambda definition
auto lam1 = [=]() { return x + y; };
x = 99;
lam1();  // returns 30, not 119 — captured the old values

// [&]: captures references — sees live values
auto lam2 = [&]() { return x + y; };
lam2();  // returns 119 — x was changed to 99

// Dangling reference trap: returned lambda outlives captured variable
auto make_bad = [](int n) {
    return [&n]() { return n; };  // n is destroyed when make_bad returns!
};                                 // calling the result is UB
Globals and statics are not captured. Only automatic-storage-duration variables (locals) can be captured. Global and static variables are accessible inside a lambda body directly — no capture needed. If you try to explicitly name a global in the capture list, GCC warns and Clang errors.
// Capturing this — accessing member variables from a lambda inside a method
struct Widget {
    int value = 42;

    void process() {
        // [this]: captures pointer to this — access to member variable, not a copy
        auto lam = [this]() { return value; };  // ok, reads this->value

        // [*this] (C++17): captures the ENTIRE object by value — safe if Widget
        // may be destroyed before the lambda is called (async scenarios)
        auto safe = [*this]() { return value; }; // reads copy of value at capture time
    }
};

The mutable keyword

By default, the generated operator() is const, so captured-by-value members are read-only inside the body. The mutable specifier removes the const, allowing you to modify those copies. Crucially, modifying a captured copy does not affect the original variable — only the member of the closure object. Modifying by-reference captures never needs mutable.

int count = 0;

// Without mutable: can't modify captured-by-value x
auto lam = [count]() mutable {
    ++count;          // modifies the closure member, NOT the outer count
    return count;
};

lam(); // returns 1
lam(); // returns 2  (closure object carries its own state)
// count is still 0 in the outer scope

// Note: mutable requires () before it — [count] mutable { } won't compile
// Also: a mutable lambda must not be a const closure object:
// const auto lam2 = [x]() mutable { ++x; };
// lam2();  // ERROR: can't call non-const operator() on const object

auto vs std::function

Since closure types are unique and unnamed, you have two ways to store a lambda: use auto to let the compiler deduce the exact closure type, or wrap it in std::function<R(Args...)>. The difference in cost is significant.

// auto: zero-overhead, exact type, smallest possible size
const auto lam = [](int x) noexcept { return x * 2; };
// sizeof(lam) == 1 (stateless closure: just an empty class)

// std::function: type-erased wrapper — handles any compatible callable
std::function<int(int)> fn = [](int x) noexcept { return x * 2; };
// sizeof(fn) == 32 (GCC): stores vtable, function pointer, possible heap allocation

Prefer auto for local lambdas

The compiler deduces the closure type exactly. No heap, no indirection, no overhead. The result is as fast as an inlined function call.

Use std::function when you need runtime type erasure

Storing heterogeneous callables in a vector, returning a lambda from a non-template function, or passing to a non-template API. The 32-byte cost is worth it in these cases.

std::function cannot hold move-only lambdas

If a lambda captures a unique_ptr by move, the closure type is not copyable. std::function requires copyable callables — use the auto-return pattern instead.

// Return a lambda from a function — before C++14 needed std::function
// C++14+: auto return type deduction works for lambdas
auto make_multiplier(int factor) noexcept {
    return [factor](int x) noexcept { return x * factor; };
    // sizeof of returned closure == sizeof(int) — just the captured int
}

// C++11: had to wrap in std::function (heavier, can't be noexcept)
std::function<int(int)> make_multiplier_old(int factor) {
    return [factor](int x) { return x * factor; };
}

IIFE — Immediately Invoked Functional Expression

A lambda can be defined and called in the same expression by appending ()immediately after the body. This pattern, borrowed from JavaScript, is useful for initializing a const variable with logic that would otherwise require a mutable temporary or a helper function.

// Without IIFE: val must be non-const while we build it
std::string val;
if (use_feature_a)
    val = compute_a();
else
    val = compute_b();

// With IIFE: val can be const, logic stays local
const std::string val = [&]() -> std::string {
    if (use_feature_a) return compute_a();
    return compute_b();
}();  // <-- immediate call

// Practical: const init with multiple conditions
const int status = [&]() noexcept {
    if (!db.connected()) return -1;
    if (!db.has_table("users")) return -2;
    return 0;
}();

C++14 — generic lambdas and init captures

C++14 added two major features. Generic lambdas allow auto in the parameter list, making the generated operator() a function template. Each auto parameter becomes an independent template type parameter. This is more powerful than regular function templates in one way: the lambda body is defined right at the call site, so it is easy to read in context.

// Generic lambda: works with any type that supports operator+
const auto add = [](auto x, auto y) { return x + y; };
add(1, 2);            // int + int
add(1.5, 2.5f);       // double + float
add(std::string{"a"}, "b");  // string concatenation

// Equivalent generated closure type:
struct {
    template<typename T, typename U>
    auto operator()(T x, U y) const { return x + y; }
};

// auto&& for perfect forwarding — forwards lvalues as lvalues, rvalues as rvalues
const auto callFoo = [](auto&& arg) {
    foo(std::forward<decltype(arg)>(arg));
};

// Variadic generic lambda: accepts any number of arguments
const auto sum = [](auto... args) {
    return (args + ...);  // C++17 fold expression
};
sum(1, 2, 3, 4.5);  // 10.5

Init captures (also called generalized captures) let you create new member variables in the closure with their own initializer, rather than just capturing existing variables. The critical use case is capturing move-only types: in C++11, you could only capture a unique_ptr by reference. With init captures, you can move it into the closure.

// Init capture: create a new closure member from an expression
int x = 30, y = 12;
const auto lam = [z = x + y]() { std::cout << z; };  // z == 42, frozen
x = 0; y = 0;
lam();  // prints 42 — z was initialized when lambda was defined

// Moving a unique_ptr into a closure (C++11 couldn't do this)
auto p = std::make_unique<Widget>();
auto task = [ptr = std::move(p)]() {
    ptr->process();   // sole owner inside the lambda
};
// p is null after the move; task owns the widget

// Optimization: precompute an expensive expression once at lambda definition
const auto prefix = std::string{"foo"};
const auto check = [savedKey = prefix + "bar"](const std::string& s) {
    return s == savedKey;  // savedKey computed once, not on every call
};

C++17 and C++20 additions

C++17: constexpr lambdas and [*this]

Lambda bodies are implicitly constexpr if they satisfy the requirements. You can also mark them explicitly. Separately, [*this]captures the entire object by value — critical when a lambda may outlive the object that created it (async callbacks, stored lambdas).

// constexpr lambda: can be called in compile-time contexts
constexpr auto square = [](int x) constexpr { return x * x; };
static_assert(square(7) == 49);  // evaluated at compile time

// [*this]: safe for async use where the object may be destroyed
struct Connection {
    std::string host_;
    void send_async(std::function<void()> cb);

    void start() {
        // [*this]: copies the entire Connection object into the closure
        // The async callback is safe even if *this is destroyed before it fires
        send_async([*this]() { use(host_); });
    }
};

C++17: the overloaded pattern

C++17's variadic template inheritance + using declarations make it possible to compose multiple lambdas into a single overload set. This is the canonical pattern for std::visit on std::variant.

// The overloaded helper — derive from multiple lambdas, expose all operator()s
template<typename... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<typename... Ts> overloaded(Ts...) -> overloaded<Ts...>; // C++17 CTAD guide

// Usage: visit a std::variant with per-type handlers
std::variant<int, std::string, double> v = "hello";

std::visit(overloaded{
    [](int x)               { std::cout << "int: " << x; },
    [](const std::string& s){ std::cout << "str: " << s; },
    [](double d)            { std::cout << "dbl: " << d; },
}, v);  // prints "str: hello"

C++20: template lambdas

C++14 generic lambdas use auto parameters, which are convenient but give no name to the type. C++20 adds explicit template parameters in the lambda head, enabling constraints, multiple uses of the same type, and concepts.

// C++14 generic: no control over type relationship
auto swap14 = [](auto& a, auto& b) { std::swap(a, b); };
// a and b could be different types — probably unintended

// C++20 template lambda: T explicitly declared, a and b must be the same type
auto swap20 = []<typename T>(T& a, T& b) { std::swap(a, b); };

// With concepts:
auto print_range = []<std::ranges::range R>(R&& r) {
    for (auto& x : r) std::cout << x << ' ';
};

Stateless lambdas and function pointers

A lambda with no captures is stateless — its closure type is an empty class with no data members. The compiler provides an implicit conversion operator to the corresponding function pointer type. This makes stateless lambdas directly usable with C-style APIs that accept function pointers.

// C API that requires a function pointer:
void c_sort(int* arr, size_t n, int(*cmp)(const void*, const void*));

// Stateless lambda implicitly converts to function pointer:
c_sort(arr, n, [](const void* a, const void* b) noexcept {
    return *static_cast<const int*>(a) - *static_cast<const int*>(b);
});

// The + trick: force conversion to function pointer explicitly
auto fn_ptr = +[](int x) { return x * 2; };  // fn_ptr is int(*)(int), not a closure
// Useful for reducing template instantiations when the same lambda is passed to
// a templated function multiple times

Rules to remember

Use auto for local lambdas, std::function when you need type erasure

auto gives a zero-overhead closure; std::function costs 32+ bytes and a potential heap allocation. The only reason to reach for std::function is runtime polymorphism over callables.

Capture by value = snapshot at definition; capture by reference = live view

Value captures are frozen at the point the lambda is written. Reference captures reflect live state but do not extend lifetimes — dangling references are the primary lambda bug.

Prefer explicit captures over [=] and [&]

Default captures hide what the lambda depends on and prevent the compiler from warning when you accidentally capture globals, statics, or this indirectly.

mutable lets you modify by-value captures without affecting the original

Modifying the copy inside the closure changes only the closure member. The outer variable is unchanged. Also, a mutable lambda's closure object must not be const.

Use [*this] for lambdas that may outlive the object

Capturing [this] is only safe if the lambda is used within the lifetime of the object. For async callbacks or stored lambdas, [*this] (C++17) copies the object into the closure.

Init captures (C++14) enable moving unique_ptr into a lambda

[ptr = std::move(p)] moves the unique_ptr into the closure. The lambda becomes the sole owner. std::function cannot hold such a lambda because it requires a copyable callable.

Sign in to track progress