C++ lambda expressions closures captures tutorial
|

C++ Lambda Expressions: Complete Guide to Closures & Captures 2026

1. What Are Lambda Expressions?

Lambda expressions, introduced in C++11, let you define anonymous functions inline — right where you need them. They’re the reason STL algorithms became practical to use. Before lambdas, you had to write separate function objects or use clunky function pointers.

// Before lambdas: need a separate functor
struct IsEven {
    bool operator()(int n) const { return n % 2 == 0; }
};
auto it = std::find_if(v.begin(), v.end(), IsEven{});

// With lambdas: inline and clear
auto it = std::find_if(v.begin(), v.end(),
                        [](int n) { return n % 2 == 0; });

2. Lambda Syntax Breakdown

Every lambda has this structure:

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

// Minimal lambda
auto greet = []() { std::cout << "Hello!\n"; };
greet(); // prints "Hello!"

// With parameters
auto add = [](int a, int b) { return a + b; };
int result = add(3, 4); // 7

// With explicit return type
auto divide = [](double a, double b) -> double {
    if (b == 0) return 0.0;
    return a / b;
};

// Parameters with default values (C++14)
auto power = [](double base, int exp = 2) {
    double result = 1.0;
    for (int i = 0; i < exp; ++i) result *= base;
    return result;
};
power(3);    // 9.0
power(2, 10); // 1024.0

The parts:

  • [captures] — what variables from the enclosing scope the lambda can access
  • (parameters) — input parameters (can be omitted if empty in C++23)
  • specifiersmutable, constexpr, noexcept, etc.
  • -> return_type — explicit return type (usually deduced automatically)
  • { body } — the function body

3. Capture Modes Explained

Captures let lambdas access variables from the surrounding scope. This is what makes them closures:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    int threshold = 50;
    std::string prefix = "Value: ";

    // [=] capture all by value (copy)
    auto byVal = [=]() {
        std::cout << prefix << threshold << "\n";
        // threshold and prefix are copies
    };

    // [&] capture all by reference
    int count = 0;
    auto byRef = [&](int n) {
        if (n > threshold) ++count; // modifies original count
    };

    // [x] capture specific variable by value
    auto specific = [threshold](int n) { return n > threshold; };

    // [&x] capture specific variable by reference
    auto specRef = [&count](int n) { count += n; };

    // Mix captures
    auto mixed = [threshold, &count](int n) {
        if (n > threshold) ++count;
        // threshold is a copy, count is a reference
    };

    // [=, &x] all by value except x by reference
    auto mostVal = [=, &count]() {
        std::cout << prefix << threshold << "\n";
        ++count;
    };

    // [&, x] all by reference except x by value
    auto mostRef = [&, threshold]() {
        ++count; // reference
        // threshold is a copy here
    };

    std::vector<int> nums = {10, 60, 30, 80, 45, 90};
    std::for_each(nums.begin(), nums.end(), byRef);
    std::cout << "Count above " << threshold << ": " << count << std::endl;
}
Dangling Reference Warning: If a lambda captures by reference and outlives the referenced variable, you get undefined behavior. Always capture by value for lambdas stored in std::function or returned from functions.
// DANGEROUS: dangling reference
std::function<int()> makeCounter() {
    int count = 0;
    return [&count]() { return ++count; }; // BUG: count destroyed!
}

// SAFE: capture by value
std::function<int()> makeCounter() {
    int count = 0;
    return [count]() mutable { return ++count; }; // OK: owns copy
}

4. Mutable Lambdas

By default, a lambda’s operator() is const, meaning captured-by-value variables can’t be modified. Use mutable to allow modification:

#include <iostream>
#include <functional>

int main() {
    int seed = 0;

    // Error without mutable:
    // auto counter = [seed]() { return ++seed; }; // won't compile

    // With mutable: can modify the captured copy
    auto counter = [seed]() mutable { return ++seed; };

    std::cout << counter() << "\n"; // 1
    std::cout << counter() << "\n"; // 2
    std::cout << counter() << "\n"; // 3
    std::cout << "Original seed: " << seed << "\n"; // still 0

    // Practical: stateful callback
    auto makeAccumulator = [](double initial) {
        return [total = initial](double val) mutable {
            total += val;
            return total;
        };
    };

    auto acc = makeAccumulator(100.0);
    std::cout << acc(10) << "\n";  // 110
    std::cout << acc(25) << "\n";  // 135
}

5. Generic Lambdas (C++14/20)

Use auto parameters to create lambdas that work with any type:

#include <iostream>
#include <string>
#include <vector>

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

    // Generic comparison
    auto greater = [](const auto& a, const auto& b) { return a > b; };
    std::cout << greater(5, 3) << "\n";          // true
    std::cout << greater(1.5, 2.5) << "\n";      // false

    // C++20: template lambda syntax
    auto typedPrint = []<typename T>(const std::vector<T>& vec) {
        for (const auto& elem : vec)
            std::cout << elem << " ";
        std::cout << "\n";
    };

    typedPrint(std::vector<int>{1, 2, 3});
    typedPrint(std::vector<std::string>{"a", "b", "c"});

    // C++20: constrained template lambda
    auto addNumbers = []<typename T>(T a, T b) requires std::integral<T> {
        return a + b;
    };
    addNumbers(1, 2);     // OK
    // addNumbers(1.0, 2.0); // Error: not integral
}

6. Immediately Invoked Lambdas

Call a lambda right where you define it — useful for complex initialization:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    // IIFE for const initialization with complex logic
    const auto config = []() {
        struct Config {
            int port;
            std::string host;
            bool debug;
        };
        // Could read from file, env vars, etc.
        Config c;
        c.port = 8080;
        c.host = "localhost";
        c.debug = true;
        return c;
    }(); // Note the () — invokes immediately

    std::cout << config.host << ":" << config.port << "\n";

    // IIFE to initialize const with conditional logic
    int input = 42;
    const std::string category = [&]() {
        if (input < 0) return "negative";
        if (input == 0) return "zero";
        if (input < 100) return "small";
        return "large";
    }();

    std::cout << "Category: " << category << "\n";
}

7. Lambdas with STL Algorithms

Lambdas unlock the full power of the STL:

#include <algorithm>
#include <numeric>
#include <vector>
#include <string>
#include <iostream>
#include <map>

struct Employee {
    std::string name;
    std::string dept;
    double salary;
};

int main() {
    std::vector<Employee> team = {
        {"Alice", "Engineering", 120000},
        {"Bob", "Marketing", 85000},
        {"Charlie", "Engineering", 135000},
        {"Diana", "Marketing", 92000},
        {"Eve", "Engineering", 110000}
    };

    // Sort by salary descending
    std::sort(team.begin(), team.end(),
              [](const Employee& a, const Employee& b) {
                  return a.salary > b.salary;
              });

    // Filter engineers
    std::vector<Employee> engineers;
    std::copy_if(team.begin(), team.end(), std::back_inserter(engineers),
                 [](const Employee& e) { return e.dept == "Engineering"; });

    // Calculate average engineering salary
    double avgSalary = std::accumulate(
        engineers.begin(), engineers.end(), 0.0,
        [](double sum, const Employee& e) { return sum + e.salary; }
    ) / engineers.size();

    // Find who earns closest to average
    auto closest = std::min_element(engineers.begin(), engineers.end(),
        [avgSalary](const Employee& a, const Employee& b) {
            return std::abs(a.salary - avgSalary) < std::abs(b.salary - avgSalary);
        });

    // Count high earners
    auto highEarners = std::count_if(team.begin(), team.end(),
        [](const Employee& e) { return e.salary > 100000; });

    std::cout << "Avg engineering salary: $" << avgSalary << "\n";
    std::cout << "Closest to avg: " << closest->name << "\n";
    std::cout << "High earners: " << highEarners << std::endl;
}

8. Advanced Lambda Patterns

Recursive Lambdas

#include <functional>
#include <iostream>

int main() {
    // Use std::function for recursive lambdas
    std::function<int(int)> factorial = [&factorial](int n) -> int {
        return n <= 1 ? 1 : n * factorial(n - 1);
    };
    std::cout << factorial(5) << "\n"; // 120

    // C++23 deducing this (no std::function overhead)
    // auto factorial = [](this auto& self, int n) -> int {
    //     return n <= 1 ? 1 : n * self(n - 1);
    // };
}

Lambda as Comparator in Containers

#include <set>
#include <map>
#include <iostream>

int main() {
    // Lambda as set comparator
    auto caseInsensitive = [](const std::string& a, const std::string& b) {
        return std::lexicographical_compare(
            a.begin(), a.end(), b.begin(), b.end(),
            [](char c1, char c2) { return tolower(c1) < tolower(c2); });
    };

    std::set<std::string, decltype(caseInsensitive)> names(caseInsensitive);
    names.insert("Alice");
    names.insert("alice"); // won't insert — same as "Alice"
    names.insert("Bob");

    for (const auto& n : names) std::cout << n << " ";
    // Output: Alice Bob
}

Storing Lambdas

#include <functional>
#include <vector>

int main() {
    // auto for single lambda
    auto square = [](int n) { return n * n; };

    // std::function for type-erased storage
    std::function<int(int)> op = square;
    op = [](int n) { return n * 2; }; // can reassign

    // Vector of callbacks
    std::vector<std::function<void()>> tasks;
    tasks.push_back([]() { std::cout << "Task 1\n"; });
    tasks.push_back([]() { std::cout << "Task 2\n"; });

    for (auto& task : tasks) task();
}

9. Practice Exercises

Exercise 1: Lambda Calculator

Create a map of string-to-lambda that maps operator names ("+", "-", "*", "/") to lambdas performing those operations.

Exercise 2: Event System

Build a simple event system using std::vector<std::function> where you can register lambda callbacks and fire events.

Exercise 3: Pipeline Builder

Create a function that takes a vector and a variable number of transform lambdas, applying them in sequence.

What's Next?

With lambdas mastered, you're ready to learn auto & Type Deduction — understanding how the compiler deduces types in modern C++, which works hand-in-hand with lambdas and templates.

Return to the C++ Learning Roadmap to continue your journey.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *