Skip to content
C++
Language
since C++14
Intermediate

Lambda Advanced Features

C++ lambda advanced features — template lambdas, perfect forwarding, IIFE, recursive lambdas, overload pattern, constexpr, consteval, static lambdas, and C++23 additions.

Advanced Lambda Expressionssince C++14

Advanced lambda features extend the basic closure mechanism with generic and template-parameterized call operators, init captures for move-only types, immediate invocation, recursive self-reference via deducing this, and compile-time evaluation — each governed by rules that accumulated from C++14 through C++23.

Overview

Lambdas began in C++11 as syntactic shorthand for local function objects. Each subsequent standard added meaningful capability: C++14 brought generic (auto) parameters and init captures; C++17 made lambdas implicitly constexpr when the body qualified; C++20 added explicit template parameters, consteval, and cleaner this-capture semantics; C++23 introduced static lambdas and relaxed the requirement for () in certain positions.


Syntax

The full lambda declarator (all specifiers optional, in order):

cpp
[capture-list](params) specifiers exception-spec -> return-type { body }
//             ^^^^^^   ^^^^^^^^
//             C++11    mutable (C++11), constexpr (C++17), consteval (C++20),
//                      static (C++23), noexcept, [[attributes]]

C++20 additionally permits a template parameter list between the capture list and the parameter list:

cpp
[capture-list]<template-params>(params) specifiers { body }  // C++20

Examples

Template Lambdas (C++20)

C++14 generic lambdas use auto parameters, each deduced independently. When two arguments must share a type, or when a concept constraint is needed, C++20 template lambdas are the right tool:

cpp
// C++14: each auto is a separate, unrelated template parameter
auto add14 = [](auto a, auto b) { return a + b; };
add14(1, 2.0);   // ok: a=int, b=double — mixed types allowed

// C++20: relate arguments through a shared T
auto add20 = []<typename T>(T a, T b) { return a + b; };
// add20(1, 2.0);  // error: conflicting deductions for T
add20(1, 2);       // ok: T=int

// C++20: constrained with a concept
auto sum_range = []<std::ranges::range R>(const R& r) {
    return std::reduce(r.begin(), r.end());
};

// C++20: variadic template lambda with perfect forwarding (no decltype trick needed)
auto forward_all = []<typename... Ts>(Ts&&... args) {
    return some_func(std::forward<Ts>(args)...);
};

Perfect forwarding in C++14 generic lambdas requires a decltype workaround because there is no named template parameter to pass to std::forward. C++20 eliminates this:

cpp
// C++14: verbose and error-prone
auto fwd14 = [](auto&& x) {
    return process(std::forward<decltype(x)>(x));
};

// C++20: clean and correct
auto fwd20 = []<typename T>(T&& x) {
    return process(std::forward<T>(x));
};

IIFE — Immediately Invoked Lambda

Invoking a lambda immediately at the point of definition initializes a const variable through arbitrary logic without polluting scope with a named helper:

cpp
// Complex const initialization
const auto primes_under_100 = []{
    std::vector<int> v;
    for (int i = 2; i < 100; ++i)
        if (is_prime(i)) v.push_back(i);
    return v;
}();  // () invokes immediately; type deduced as std::vector<int>

// Multi-step construction that must remain atomic
const auto conn = [&]() -> DatabaseConnection {
    auto c = DatabaseConnection{host, port};
    c.set_timeout(5s);
    c.authenticate(creds);
    return c;
}();

// constexpr IIFE — evaluate a loop at compile time (C++17)
constexpr int triangle_10 = []{
    int sum = 0;
    for (int i = 1; i <= 10; ++i) sum += i;
    return sum;
}();
static_assert(triangle_10 == 55);

Recursive Lambdas

A lambda cannot refer to itself by name. Three approaches exist, with different costs:

cpp
// C++23 (deducing this) — zero overhead, cleanest call site
auto factorial = [](this auto self, int n) -> int {
    return n <= 1 ? 1 : n * self(n - 1);
};
factorial(10);  // 3628800

// C++14: pass self by reference — requires explicit return type, awkward call site
auto fib = [](auto& self, int n) -> int {
    return n <= 1 ? n : self(self, n-1) + self(self, n-2);
};
fib(fib, 10);  // 55

// std::function — readable but adds type-erasure overhead and heap allocation
std::function<int(int)> fact;
fact = [&](int n) -> int { return n <= 1 ? 1 : n * fact(n-1); };
fact(5);  // 120

Prefer the deducing-this form in C++23. Use std::function only as a last resort.


mutable, constexpr, and consteval

cpp
// mutable: call operator becomes non-const; value-captured copies are modifiable
auto counter = [n = 0]() mutable { return ++n; };
counter(); counter(); counter();  // 1, 2, 3 — no external state

// C++17: lambda is implicitly constexpr if the body qualifies
constexpr auto square = [](int x) { return x * x; };
static_assert(square(7) == 49);

// C++17: explicit constexpr specifier (redundant but self-documenting)
constexpr auto cube = [](int x) constexpr { return x * x * x; };

// C++20: consteval — compile-time only; runtime call is a hard error
auto at_compile_time = []() consteval { return 42; };
constexpr int x = at_compile_time();   // ok
// int y = at_compile_time();           // error: not a constant expression context

Stateless Lambdas and Function Pointers

A captureless lambda is implicitly convertible to a matching function pointer. This is the primary bridge to C APIs:

cpp
// Implicit conversion
int (*cmp)(int, int) = [](int a, int b) { return a - b; };

// Comparator for a C sorting API (no captures allowed)
qsort(data, n, sizeof(int), [](const void* a, const void* b) {
    return *static_cast<const int*>(a) - *static_cast<const int*>(b);
});

// C++23: static lambda — stateless by declaration, always pointer-convertible
auto make_id = [](std::uint32_t seed) static { return seed * 2654435761u; };

The static specifier (C++23) makes stateless intent explicit: the compiler rejects any capture.


Capture Edge Cases

cpp
// Init capture: expression evaluated once at capture time
auto deferred = [result = compute_expensive()]() { return result * 2; };

// Move-only types in captures — only possible via init capture (C++14)
auto ptr = std::make_unique<Resource>(42);
auto owner = [p = std::move(ptr)]() { return p->value(); };
// ptr is null here; owner holds the unique_ptr

// this in member functions
struct Widget {
    int id_;

    auto unsafe_handler() {
        return [=]() { return id_; };     // C++11/14/17: captures 'this' by pointer!
    }

    auto safe_copy() {
        return [*this]() { return id_; }; // C++17: copies entire *this — safe if Widget dies
    }

    auto explicit_pointer() {
        return [=, this]() { return id_; }; // C++20: pointer capture, but stated explicitly
    }
};

[=] in a member function captures this by pointer, not by value — a silent dangling-pointer hazard when the lambda outlives the object. Prefer [*this] (C++17) for safety.


Overload Pattern (C++17 / C++20)

Inheriting from multiple lambdas and pulling all their call operators into scope creates an overload set — the standard technique for exhaustive std::visit on std::variant:

cpp
// C++17: requires an explicit deduction guide
template<typename... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<typename... Ts> overloaded(Ts...) -> overloaded<Ts...>;  // C++17 only

// C++20: aggregate CTAD makes the deduction guide unnecessary
auto handler = overloaded{
    [](int i)         { std::println("int: {}", i); },
    [](std::string s) { std::println("string: {}", s); },
    [](auto x)        { std::println("other: {}", x); },  // fallback
};

std::variant<int, std::string, double> v = 3.14;
std::visit(handler, v);  // "other: 3.14"

The using Ts::operator()... pack expansion (C++17) brings every inherited call operator into the derived class's overload set.


C++23: () Omission with Specifiers

Before C++23, any specifier after the capture list required an explicit () even with no parameters. C++23 removes this restriction:

cpp
// C++23: () can be omitted when no parameters are present
auto f = [] mutable { return state++; };
auto g = [] noexcept { return 42; };
auto h = [] -> int { return 0; };     // trailing return type also qualifies
auto s = [] static { return now(); }; // C++23 static lambda

// Pre-C++23: () was mandatory alongside any specifier
auto f2 = []() mutable { return state++; };  // valid in all standards

Best Practices

  • Prefer [*this] (C++17) over [=] in member functions whenever the lambda may outlive the object.
  • Use C++20 template lambdas ([]<typename T>) rather than auto parameters when types must be related, constrained, or forwarded cleanly.
  • Use deducing-this (C++23) for recursive lambdas instead of std::function — identical ergonomics, zero runtime overhead.
  • Mark stateless utility lambdas static (C++23) to communicate intent and prevent accidental capture.
  • For std::visit exhaustiveness, the overload pattern with a general auto fallback is cleaner than a chain of if constexpr.

Common Pitfalls

[=] silently captures this by pointer. In C++14 and C++17, [=] in a class method captures the implicit this pointer, not a copy of *this. The lambda silently dangles if the object is destroyed first. Use [*this] or [=, this] to make the semantics explicit.

mutable does not affect reference captures. [&x]() mutable does nothing useful for xmutable only removes const from the call operator's implicit object, which matters only for value-captured copies. Reference captures are always modifiable regardless.

constexpr lambda is not consteval. A constexpr lambda can be called at runtime without error. Only consteval (C++20) forces compile-time evaluation; a runtime call to a consteval lambda is a hard compiler error.

Overload pattern ambiguity. If two overloads are equally ranked for an argument type, the call is ambiguous. Add requires clauses or reorder from most-specific to least-specific, ending with a general auto fallback.

std::function recursion overhead. Capturing std::function by reference for recursion adds a virtual dispatch and potential heap allocation on every call. For performance-sensitive recursive lambdas, use the deducing-this form (C++23) or the self-reference trick (C++14).


See Also