Skip to content

Lambdas & Functional Programming

Master C++ lambdas — capture lists, mutable lambdas, generic lambdas, and their use as algorithm predicates. Learn std::function, std::invoke, and the IIFE pattern.

Lambda Expressions

A lambda expression creates an anonymous function object (closure) inline. Lambdas are the primary way to pass behaviour to STL algorithms, callbacks, and event handlers.

The full syntax is:

[captures](parameters) mutable -> return_type { body }

- Captures — variables from the enclosing scope to use inside the lambda
- Parameters — like regular function parameters
- mutable — allows modifying captured-by-value variables
- Return type — usually deduced automatically
- Body — the function body

Lambdas compile down to compiler-generated function objects (structs with operator()) — they have zero overhead compared to hand-written functors. Anonymous, efficient, and quietly getting on with it.

Basic Lambda Syntax and Captures

The capture list is the defining feature of lambdas. It determines which enclosing variables the lambda can access and how.

lambda_captures.cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 5, 3, 8, 2, 9, 4, 7, 6};

    // Simplest lambda — no captures
    std::sort(nums.begin(), nums.end(),
              [](int a, int b) { return a > b; });  // descending

    // Capture by value [=] — copies variables
    int threshold = 5;
    auto above = std::count_if(nums.begin(), nums.end(),
                               [threshold](int x) { return x > threshold; });
    std::cout << "Above " << threshold << ": " << above << '\n';

    // Capture by reference [&] — references variables
    int sum = 0;
    std::for_each(nums.begin(), nums.end(),
                  [&sum](int x) { sum += x; });
    std::cout << "Sum: " << sum << '\n';

    // Mixed captures
    int min_val = 3, max_val = 7;
    auto in_range = std::count_if(nums.begin(), nums.end(),
        [min_val, &max_val](int x) { return x >= min_val && x <= max_val; });
    // min_val captured by value, max_val captured by reference

    // Capture all by value [=] or all by reference [&]
    auto print_all = [&]() {  // captures everything by reference
        for (int n : nums) std::cout << n << ' ';
        std::cout << '\n';
    };
    print_all();

    // Init capture (C++14) — create new variables in the capture
    auto counter = [count = 0]() mutable { return ++count; };
    std::cout << counter() << '\n';  // 1
    std::cout << counter() << '\n';  // 2
    std::cout << counter() << '\n';  // 3
}

Generic and Template Lambdas

C++14 introduced generic lambdas with auto parameters. C++20 extended this with explicit template parameter lists, allowing full template power in lambdas.

generic_lambdas.cpp
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
#include <type_traits>

int main() {
    // C++14: generic lambda — auto parameters
    auto print = [](const auto& x) {
        std::cout << x << '\n';
    };
    print(42);           // int
    print(3.14);         // double
    print("hello");      // const char*

    // C++14: generic lambda for comparisons
    auto max_of = [](const auto& a, const auto& b) {
        return (a > b) ? a : b;
    };
    std::cout << max_of(3, 7) << '\n';             // 7
    std::cout << max_of("abc", "xyz") << '\n';     // xyz (lexicographic)

    // C++20: template lambda — explicit template parameters
    auto add = []<typename T>(T a, T b) -> T {
        return a + b;
    };
    std::cout << add(1, 2) << '\n';                // 3
    std::cout << add(1.5, 2.5) << '\n';            // 4.0

    // C++20: constrained template lambda with concepts
    auto integral_only = []<std::integral T>(T x) {
        return x * 2;
    };
    std::cout << integral_only(5) << '\n';         // 10
    // integral_only(3.14);  // ERROR: double doesn't satisfy std::integral

    // Using generic lambdas with algorithms
    std::vector<std::string> words = {"banana", "apple", "cherry"};
    std::sort(words.begin(), words.end(),
              [](const auto& a, const auto& b) {
                  return a.size() < b.size();  // sort by length
              });
    for (const auto& w : words) std::cout << w << ' ';  // apple banana cherry
    std::cout << '\n';
}

std::function and std::invoke

std::function is a type-erased callable wrapper that can hold any callable with a matching signature — lambdas, function pointers, functors, member function pointers. std::invoke is a uniform way to call any callable. Think of it as a very accommodating cloakroom.

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

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

struct Multiplier {
    int factor;
    int operator()(int x) const { return x * factor; }
    int multiply(int x) const { return x * factor; }
};

int main() {
    // std::function can hold different callable types
    std::function<int(int, int)> op;

    op = add;                                        // function pointer
    std::cout << op(3, 4) << '\n';                   // 7

    op = [](int a, int b) { return a * b; };         // lambda
    std::cout << op(3, 4) << '\n';                   // 12

    // Store callables in a collection
    std::vector<std::function<int(int)>> transforms;
    transforms.push_back([](int x) { return x + 1; });
    transforms.push_back([](int x) { return x * 2; });
    transforms.push_back(Multiplier{10});            // functor

    int val = 5;
    for (auto& fn : transforms) {
        val = fn(val);
    }
    std::cout << val << '\n';  // ((5+1)*2)*10 = 120

    // std::invoke — uniform call syntax for any callable
    Multiplier m{3};
    std::cout << std::invoke(add, 10, 20)       << '\n';  // 30
    std::cout << std::invoke(m, 7)              << '\n';  // 21
    std::cout << std::invoke(&Multiplier::multiply, m, 7) << '\n';  // 21
    std::cout << std::invoke(&Multiplier::factor, m)      << '\n';  // 3 (data member)
}

The IIFE Pattern

IIFE (Immediately Invoked Function Expression) is a pattern borrowed from JavaScript. You define a lambda and call it immediately. This is useful for complex initialisation of const variables:

const auto config = [&]() {
    Config c;
    c.host = read_env("HOST");
    c.port = parse_int(read_env("PORT"));
    if (c.port <= 0) c.port = 8080;
    c.debug = is_debug_build();
    return c;
}();  // <-- note the () — invoked immediately

Without IIFE, you would either need a non-const variable (losing immutability guarantees) or a separate factory function. IIFE keeps the initialisation logic local and the result const.

Pitfall

Capturing by reference is dangerous when the lambda outlives the captured variable:

std::function<int()> make_counter() {
    int count = 0;
    return [&count]() { return ++count; };  // BUG: count is destroyed!
}

The returned lambda holds a reference to a local variable that no longer exists. This is undefined behaviour, and it will not end well. Fix it by capturing by value (with mutable if needed):

std::function<int()> make_counter() {
    int count = 0;
    return [count]() mutable { return ++count; };  // OK: owns a copy
}

Rule of thumb: capture by reference for short-lived lambdas (algorithm predicates), capture by value for long-lived lambdas (callbacks, stored in data structures).

Best Practice

std::function has overhead: it allocates memory (for large closures), uses virtual dispatch, and prevents inlining. For algorithm predicates and other cases where the callable is used immediately, pass the lambda directly — the compiler can inline it completely.

Use std::function only when you need to:
- Store callables for later use
- Type-erase different callable types into a uniform container
- Pass callables across ABI boundaries

In templates, use auto or concepts to accept callables without type erasure. Save the heavy machinery for when it is genuinely needed:

template<std::invocable<int> F>
void apply(F&& fn, int value) {
    fn(value);  // no std::function overhead
}
Key Takeaways
  • Lambdas are zero-overhead anonymous function objects — use them freely with STL algorithms
  • Capture by value [x] for safety with long-lived lambdas; capture by reference [&x] for short-lived ones
  • C++14 generic lambdas (auto params) and C++20 template lambdas give full generic programming power
  • std::function type-erases callables but has overhead — prefer auto or templates when possible
  • The IIFE pattern [&]() { ... }() enables complex initialisation of const variables
  • Init captures ([x = expr]) allow creating new variables in the closure (C++14)

Quiz — Test Your Knowledge

(15 XP)

1. What does the `mutable` keyword do on a lambda?

2. What is the danger of capturing a local variable by reference in a lambda that is returned from a function?

3. What does `[count = 0]() mutable { return ++count; }` demonstrate?