Lambda Expressions

From C++11 basics to C++23 enhancements

Lambda Basics

Lambdas are unnamed function objects that can capture variables from their enclosing scope.

// Basic lambda syntax
[capture](parameters) -> return_type { body }

// Examples:
auto add = [](int a, int b) { return a + b; };

auto square = [](double x) -> double { return x * x; };

// With capture
int offset = 10;
auto add_offset = [offset](int x) { return x + offset; };

Capture Modes

int x = 1, y = 2;

// [] - No capture
auto f1 = []() { return 42; };

// [=] - Capture all by value
auto f2 = [=]() { return x + y; };  // x and y copied

// [&] - Capture all by reference
auto f3 = [&]() { x++; y++; };  // Modifies originals

// [x] - Capture specific by value
auto f4 = [x]() { return x; };  // x copied

// [&x] - Capture specific by reference
auto f5 = [&x]() { x++; };  // Modifies x

// Mixed: [x, &y] - x by value, y by reference
auto f6 = [x, &y]() { y = x; };

// [this] - Capture current object by pointer
class MyClass {
    int value_;
    void method() {
        auto lambda = [this]() { return value_; };
    }
};

// C++17: [*this] - Capture current object by value
void MyClass::safe_method() {
    auto lambda = [*this]() { return value_; };  // Copy of *this
}

Generic Lambdas (C++14)

// C++14: Auto parameters
auto add = [](auto a, auto b) { return a + b; };

add(1, 2);           // int
add(1.5, 2.5);       // double
add(std::string("a"), "b");  // string

// Multiple auto parameters (can be different types)
auto multiply = [](auto x, auto y) { return x * y; };
multiply(2, 3.14);  // int * double = double

// With trailing return type
auto divide = [](auto a, auto b) -> double {
    return static_cast<double>(a) / b;
};

Mutable Lambdas

// Without mutable: capture by value is const
int counter = 0;
auto bad_increment = [counter]() {
    // counter++;  // ERROR: counter is const
    return counter;
};

// With mutable: can modify captured values
auto good_increment = [counter]() mutable {
    return counter++;  // OK: modifies copy
};

good_increment();  // Returns 0, internal counter is 1
good_increment();  // Returns 1, internal counter is 2
// Original 'counter' variable is still 0!

Stateful Lambdas

// Lambdas can maintain state between calls
auto make_counter = [](int start = 0) {
    int count = start;
    return [count]() mutable {
        return count++;
    };
};

auto counter1 = make_counter(10);
auto counter2 = make_counter(100);

counter1();  // 10
counter1();  // 11
counter2();  // 100 (independent state)

// Custom comparator with state
auto make_case_insensitive_comparator = []() {
    return [](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 std::tolower(c1) < std::tolower(c2);
            }
        );
    };
};

Template Lambdas (C++20)

// C++20: Explicit template parameters
auto lambda = []<typename T>(T value) {
    return value * 2;
};

// With concepts
auto process = []<std::floating_point T>(T value) {
    return std::sqrt(value);
};

// Template parameter packs
auto print_tuple = []<typename... Args>(const std::tuple<Args...>& t) {
    std::apply([](auto&&... args) {
        ((std::cout << args << " "), ...);
    }, t);
};

// Constrained lambdas
auto sort = []<typename T>(std::vector<T>& v)
    requires std::totally_ordered<T>
{
    std::sort(v.begin(), v.end());
};

Recursive Lambdas

// C++23: Deducing this for recursion
auto factorial = [](this auto&& self, int n) -> int {
    if (n <= 1) return 1;
    return n * self(n - 1);
};

// Pre-C++23 workaround with std::function (has overhead)
std::function<int(int)> fib = [&](int n) -> int {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
};

// Pre-C++23 workaround with Y-combinator pattern
auto make_recursive = [](auto f) {
    return [f](auto&&... args) {
        return f(f, std::forward<decltype(args)>(args)...);
    };
};

auto factorial_old = make_recursive([](auto self, int n) -> int {
    if (n <= 1) return 1;
    return n * self(self, n - 1);
});

Best Practices

  • Prefer [=] or [&] for simple captures, explicit list for complex
  • Use [*this] in C++17 when object may outlive lambda
  • Keep lambdas small and focused
  • Consider named functions for complex logic
  • Don't capture by reference if lambda outlives the captured variables
  • Avoid default capture [&] in long/complex lambdas (unclear what's captured)