Module 6.4

Lambda Expressions

Lambda expressions are anonymous functions that you can define inline. They revolutionized C++ programming by enabling functional programming patterns and making STL algorithms incredibly powerful and expressive!

35 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Lambda syntax and components
  • Capture clauses - by value and reference
  • Generic lambdas with auto parameters
  • Lambdas with STL algorithms
  • Advanced patterns and best practices
Contents
01

Introduction to Lambdas

Lambda expressions, introduced in C++11, are one of the most significant additions to modern C++. They allow you to define anonymous functions right where you need them, eliminating the need for separate function declarations and making your code more concise and readable.

What is a Lambda?

A lambda expression is essentially an unnamed function object that you can define inline. Before lambdas, if you wanted to pass a custom operation to an STL algorithm like std::sort, you had to write a separate function or a functor class. Lambdas let you write that logic right at the point of use.

C++ Feature

Lambda Expression

A lambda expression is an anonymous function object that can capture variables from its enclosing scope. It provides a convenient way to define short, inline functions without the overhead of declaring a named function or class.

Key characteristics: Anonymous (no name required), Inline definition, Can capture local variables, Creates a closure object, Can be stored in variables or passed to functions.

Before and After Lambdas

Let's see how lambdas simplify code. Here's how you would sort a vector of strings by length before C++11 versus with lambdas:

// BEFORE C++11: Need a separate function
bool compareByLength(const string& a, const string& b) {
    return a.length() < b.length();
}

int main() {
    vector<string> words = {"apple", "pie", "banana", "kiwi"};
    sort(words.begin(), words.end(), compareByLength);
    // Result: {"pie", "kiwi", "apple", "banana"}
    return 0;
}
// WITH LAMBDAS (C++11): Define inline where needed
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

int main() {
    vector<string> words = {"apple", "pie", "banana", "kiwi"};
    
    // Lambda defined right at the point of use
    sort(words.begin(), words.end(), 
         [](const string& a, const string& b) {
             return a.length() < b.length();
         });
    
    for (const auto& w : words) cout << w << " ";
    // Output: pie kiwi apple banana
    return 0;
}
Why Use Lambdas?
  • Define functions at point of use
  • Capture local variables easily
  • Cleaner code with STL algorithms
  • No need for separate function declarations
Common Mistakes
  • Capturing by reference when object goes out of scope
  • Forgetting mutable keyword for modifications
  • Capturing too much with [=] or [&]
  • Not considering lambda lifetime
Your First Lambda

Let's start with the simplest possible lambda - one that takes no parameters and just prints a message:

#include <iostream>
using namespace std;

int main() {
    // A simple lambda stored in a variable
    auto greet = []() {
        cout << "Hello from lambda!" << endl;
    };
    
    // Call the lambda like a regular function
    greet();  // Output: Hello from lambda!
    
    // You can also call a lambda immediately
    []() { cout << "Immediate execution!" << endl; }();
    // Output: Immediate execution!
    
    return 0;
}

Practice Questions: Introduction to Lambdas

Task: Create a lambda that takes a string parameter (name) and prints "Hello, [name]!". Store it in a variable called greet and call it with "Alice".

Show Solution
#include <iostream>
#include <string>
using namespace std;

int main() {
    auto greet = [](const string& name) {
        cout << "Hello, " << name << "!" << endl;
    };
    
    greet("Alice");  // Output: Hello, Alice!
    greet("Bob");    // Output: Hello, Bob!
    
    return 0;
}

Task: Create a lambda that takes two integers and returns their sum. Use it to add 5 and 3.

Show Solution
#include <iostream>
using namespace std;

int main() {
    auto add = [](int a, int b) {
        return a + b;
    };
    
    int result = add(5, 3);
    cout << "5 + 3 = " << result << endl;  // Output: 5 + 3 = 8
    
    // Can also use directly
    cout << "10 + 20 = " << add(10, 20) << endl;  // Output: 10 + 20 = 30
    
    return 0;
}
02

Lambda Syntax

Understanding lambda syntax is crucial for writing effective modern C++. A lambda has several components, each serving a specific purpose. Let's break down the anatomy of a lambda expression.

Lambda Anatomy

A complete lambda expression consists of several parts:

// Full lambda syntax:
// [capture](parameters) mutable -> return_type { body }

auto lambda = [capture](int x, int y) mutable -> int {
    // function body
    return x + y;
};
Component Syntax Required? Description
Capture clause [] Yes Specifies which outside variables are accessible inside the lambda
Parameters (params) Optional Input parameters, like regular function parameters
Mutable mutable Optional Allows modification of captured-by-value variables
Return type -> type Optional Explicit return type (usually auto-deduced)
Body { } Yes The function body with statements to execute
Syntax Variations

Most of the time, you'll use simpler forms. Here are the common variations from minimal to complete:

#include <iostream>
using namespace std;

int main() {
    // Minimal: no parameters, no capture
    auto minimal = [] { cout << "Minimal lambda" << endl; };
    minimal();  // Output: Minimal lambda
    
    // With parameters
    auto add = [](int a, int b) { return a + b; };
    cout << "3 + 4 = " << add(3, 4) << endl;  // Output: 3 + 4 = 7
    
    // With explicit return type
    auto divide = [](double a, double b) -> double {
        if (b == 0) return 0.0;
        return a / b;
    };
    cout << "10 / 3 = " << divide(10, 3) << endl;  // Output: 10 / 3 = 3.33333
    
    // With capture (we'll cover this in detail next)
    int multiplier = 5;
    auto multiply = [multiplier](int x) { return x * multiplier; };
    cout << "7 * 5 = " << multiply(7) << endl;  // Output: 7 * 5 = 35
    
    return 0;
}
Return Type Deduction

In most cases, the compiler can automatically deduce the return type from the return statement. You only need to specify it explicitly when the return type is ambiguous or you want to be explicit:

#include <iostream>
#include <string>
using namespace std;

int main() {
    // Return type automatically deduced as int
    auto square = [](int x) { return x * x; };
    cout << "5 squared = " << square(5) << endl;  // Output: 25
    
    // Return type automatically deduced as string
    auto getMessage = [](bool success) {
        if (success) return string("Operation successful");
        return string("Operation failed");
    };
    cout << getMessage(true) << endl;  // Output: Operation successful
    
    // Explicit return type needed when returning different types
    auto getValue = [](bool flag) -> double {
        if (flag) return 42;     // int, but converted to double
        return 3.14;             // double
    };
    cout << "Value: " << getValue(true) << endl;   // Output: Value: 42
    cout << "Value: " << getValue(false) << endl;  // Output: Value: 3.14
    
    return 0;
}
Best Practice: Let the compiler deduce return types when possible. Only specify explicit return types when necessary for clarity or when the deduction would be ambiguous.

Practice Questions: Lambda Syntax

Task: Create a lambda that takes an integer and returns true if it's even, false otherwise. Use explicit return type -> bool.

Show Solution
#include <iostream>
using namespace std;

int main() {
    auto isEven = [](int n) -> bool {
        return n % 2 == 0;
    };
    
    cout << boolalpha;  // Print true/false instead of 1/0
    cout << "4 is even: " << isEven(4) << endl;  // Output: true
    cout << "7 is even: " << isEven(7) << endl;  // Output: false
    
    return 0;
}

Task: Create a lambda that takes three double parameters and returns the average. Test it with values 10.0, 20.0, and 30.0.

Show Solution
#include <iostream>
using namespace std;

int main() {
    auto average = [](double a, double b, double c) -> double {
        return (a + b + c) / 3.0;
    };
    
    double result = average(10.0, 20.0, 30.0);
    cout << "Average of 10, 20, 30 = " << result << endl;
    // Output: Average of 10, 20, 30 = 20
    
    cout << "Average of 5.5, 7.5, 2.0 = " << average(5.5, 7.5, 2.0) << endl;
    // Output: Average of 5.5, 7.5, 2.0 = 5
    
    return 0;
}
03

Capture Clauses

The capture clause is what makes lambdas truly powerful. It allows your lambda to access variables from the surrounding scope, creating what's called a "closure". Understanding capture modes is essential for writing correct and efficient lambda expressions.

Capture by Value [=]

When you capture by value, the lambda gets its own copy of the variable. Changes inside the lambda don't affect the original, and changes to the original don't affect the copy inside.

#include <iostream>
using namespace std;

int main() {
    int x = 10;
    
    // Capture x by value (makes a copy)
    auto byValue = [x]() {
        cout << "Inside lambda: x = " << x << endl;
        // x = 20;  // ERROR! Can't modify by default
    };
    
    x = 100;  // Change original
    byValue();  // Output: Inside lambda: x = 10 (still the copied value)
    
    return 0;
}
Capture by Reference [&]

Capture by reference gives the lambda direct access to the original variable. Changes inside the lambda affect the original variable, and vice versa.

#include <iostream>
using namespace std;

int main() {
    int counter = 0;
    
    // Capture counter by reference
    auto increment = [&counter]() {
        counter++;  // Modifies the original
        cout << "Counter is now: " << counter << endl;
    };
    
    increment();  // Output: Counter is now: 1
    increment();  // Output: Counter is now: 2
    increment();  // Output: Counter is now: 3
    
    cout << "Final counter: " << counter << endl;  // Output: Final counter: 3
    
    return 0;
}
Capture Clause Options
Capture Description Example
[] Capture nothing [] { return 42; }
[x] Capture x by value [x] { return x * 2; }
[&x] Capture x by reference [&x] { x++; }
[=] Capture all used variables by value [=] { return a + b; }
[&] Capture all used variables by reference [&] { a++; b++; }
[=, &x] Capture all by value, but x by reference [=, &x] { x = a + b; }
[&, x] Capture all by reference, but x by value [&, x] { a = x * 2; }
#include <iostream>
using namespace std;

int main() {
    int a = 1, b = 2, c = 3;
    
    // Capture all by value
    auto allByValue = [=]() {
        cout << "a=" << a << ", b=" << b << ", c=" << c << endl;
    };
    
    // Capture all by reference
    auto allByRef = [&]() {
        a++; b++; c++;
    };
    
    // Mixed: all by value except 'a' by reference
    auto mixed = [=, &a]() {
        a = b + c;  // Can modify a, can read b and c
    };
    
    allByValue();  // Output: a=1, b=2, c=3
    allByRef();
    cout << "After allByRef: a=" << a << ", b=" << b << ", c=" << c << endl;
    // Output: After allByRef: a=2, b=3, c=4
    
    mixed();
    cout << "After mixed: a=" << a << endl;  // Output: After mixed: a=7
    
    return 0;
}
The Mutable Keyword

By default, variables captured by value are const inside the lambda. If you need to modify the copy (without affecting the original), use the mutable keyword:

#include <iostream>
using namespace std;

int main() {
    int value = 10;
    
    // Without mutable - value is const inside lambda
    // auto test = [value]() { value++; };  // ERROR!
    
    // With mutable - can modify the copy
    auto counter = [value]() mutable {
        value++;  // Modifies the copy, not the original
        cout << "Inside: " << value << endl;
    };
    
    counter();  // Output: Inside: 11
    counter();  // Output: Inside: 12
    counter();  // Output: Inside: 13
    
    cout << "Original value: " << value << endl;  // Output: Original value: 10
    
    return 0;
}
Danger - Reference Lifetime: Be careful with capture by reference! If the lambda outlives the captured variable, you'll have a dangling reference. This commonly happens when returning lambdas from functions or storing them for later use.
// DANGEROUS: Returning lambda that captures local by reference
auto createBadLambda() {
    int localVar = 42;
    return [&localVar]() { return localVar; };  // DANGER! localVar dies when function returns
}

// SAFE: Capture by value instead
auto createGoodLambda() {
    int localVar = 42;
    return [localVar]() { return localVar; };  // OK - lambda has its own copy
}

Practice Questions: Capture Clauses

Task: Create a lambda that acts as an accumulator. It should capture a starting value (0) by value with mutable, and each call should add the parameter to the running total and return it.

Show Solution
#include <iostream>
using namespace std;

int main() {
    int total = 0;
    
    auto accumulator = [total]() mutable -> auto {
        return [total](int value) mutable {
            total += value;
            return total;
        };
    }();
    
    // Simpler version:
    auto acc = [sum = 0](int value) mutable {
        sum += value;
        return sum;
    };
    
    cout << acc(5) << endl;   // Output: 5
    cout << acc(10) << endl;  // Output: 15
    cout << acc(3) << endl;   // Output: 18
    
    return 0;
}

Task: Create a function that takes a multiplier value and returns a lambda that multiplies any number by that value. Test with multipliers 2 and 10.

Show Solution
#include <iostream>
#include <functional>
using namespace std;

function<int(int)> makeMultiplier(int factor) {
    return [factor](int x) {
        return x * factor;
    };
}

int main() {
    auto doubler = makeMultiplier(2);
    auto tenTimes = makeMultiplier(10);
    
    cout << "5 * 2 = " << doubler(5) << endl;    // Output: 5 * 2 = 10
    cout << "5 * 10 = " << tenTimes(5) << endl;  // Output: 5 * 10 = 50
    cout << "7 * 2 = " << doubler(7) << endl;    // Output: 7 * 2 = 14
    
    return 0;
}
04

Generic Lambdas

C++14 introduced generic lambdas, allowing you to use auto for parameter types. This creates a lambda that works with any type, similar to function templates but with much simpler syntax. Generic lambdas make your code more flexible and reusable.

Auto Parameters

By using auto as a parameter type, the lambda becomes a template that can accept any type:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    // Generic lambda - works with any type!
    auto print = [](auto value) {
        cout << value << endl;
    };
    
    print(42);           // Output: 42 (int)
    print(3.14);         // Output: 3.14 (double)
    print("Hello");      // Output: Hello (const char*)
    print(string("World")); // Output: World (string)
    
    // Generic lambda for addition
    auto add = [](auto a, auto b) {
        return a + b;
    };
    
    cout << add(5, 3) << endl;           // Output: 8 (int + int)
    cout << add(2.5, 1.5) << endl;       // Output: 4 (double + double)
    cout << add(string("Hello, "), string("World!")) << endl;  
    // Output: Hello, World! (string concatenation)
    
    return 0;
}
Generic Lambdas with STL

Generic lambdas are incredibly useful with STL algorithms because they can work with any container element type:

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

int main() {
    // Generic print lambda
    auto printAll = [](const auto& container) {
        for (const auto& item : container) {
            cout << item << " ";
        }
        cout << endl;
    };
    
    vector<int> numbers = {1, 2, 3, 4, 5};
    vector<string> words = {"apple", "banana", "cherry"};
    
    printAll(numbers);  // Output: 1 2 3 4 5
    printAll(words);    // Output: apple banana cherry
    
    // Generic comparison for sorting
    auto descending = [](const auto& a, const auto& b) {
        return a > b;
    };
    
    sort(numbers.begin(), numbers.end(), descending);
    sort(words.begin(), words.end(), descending);
    
    printAll(numbers);  // Output: 5 4 3 2 1
    printAll(words);    // Output: cherry banana apple
    
    return 0;
}
Perfect Forwarding in Generic Lambdas

For maximum efficiency with generic lambdas, you can use perfect forwarding to preserve value categories (lvalue/rvalue):

#include <iostream>
#include <utility>
using namespace std;

int main() {
    // Perfect forwarding generic lambda
    auto forwarder = [](auto&& arg) {
        // forward preserves whether arg was lvalue or rvalue
        return forward<decltype(arg)>(arg);
    };
    
    int x = 10;
    cout << forwarder(x) << endl;     // lvalue forwarded
    cout << forwarder(42) << endl;    // rvalue forwarded
    
    // Practical example: generic wrapper
    auto wrapper = [](auto&& func, auto&&... args) {
        cout << "Calling function..." << endl;
        return func(forward<decltype(args)>(args)...);
    };
    
    auto result = wrapper([](int a, int b) { return a + b; }, 5, 3);
    cout << "Result: " << result << endl;
    // Output: Calling function...
    //         Result: 8
    
    return 0;
}

Practice Questions: Generic Lambdas

Task: Create a generic lambda that returns the maximum of two values. Test it with integers, doubles, and strings.

Show Solution
#include <iostream>
#include <string>
using namespace std;

int main() {
    auto maximum = [](const auto& a, const auto& b) {
        return (a > b) ? a : b;
    };
    
    cout << "max(5, 3) = " << maximum(5, 3) << endl;
    // Output: max(5, 3) = 5
    
    cout << "max(3.14, 2.71) = " << maximum(3.14, 2.71) << endl;
    // Output: max(3.14, 2.71) = 3.14
    
    cout << "max(\"apple\", \"banana\") = " << maximum(string("apple"), string("banana")) << endl;
    // Output: max("apple", "banana") = banana
    
    return 0;
}

Task: Create a generic lambda that calculates the sum of all elements in any container. Test it with vector<int> and vector<double>.

Show Solution
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;

int main() {
    auto sum = [](const auto& container) {
        using T = typename remove_reference_t<decltype(*container.begin())>;
        return accumulate(container.begin(), container.end(), T{});
    };
    
    // Simpler version using auto return type deduction
    auto simpleSum = [](const auto& container) {
        auto total = *container.begin() - *container.begin();  // Get zero of correct type
        for (const auto& item : container) {
            total += item;
        }
        return total;
    };
    
    vector<int> integers = {1, 2, 3, 4, 5};
    vector<double> doubles = {1.5, 2.5, 3.5};
    
    cout << "Sum of integers: " << simpleSum(integers) << endl;
    // Output: Sum of integers: 15
    
    cout << "Sum of doubles: " << simpleSum(doubles) << endl;
    // Output: Sum of doubles: 7.5
    
    return 0;
}
05

Lambdas with STL Algorithms

Lambdas truly shine when combined with STL algorithms. They let you customize algorithm behavior inline without creating separate functions or functor classes. This combination is the heart of modern C++ programming style.

Custom Sorting

One of the most common uses of lambdas is providing custom comparison functions to sorting algorithms:

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

int main() {
    // Sort strings by length
    vector<string> words = {"elephant", "cat", "dog", "butterfly", "ant"};
    
    sort(words.begin(), words.end(), [](const string& a, const string& b) {
        return a.length() < b.length();
    });
    
    for (const auto& w : words) cout << w << " ";
    cout << endl;
    // Output: cat dog ant elephant butterfly
    
    // Sort numbers in descending order
    vector<int> numbers = {5, 2, 8, 1, 9, 3};
    
    sort(numbers.begin(), numbers.end(), [](int a, int b) {
        return a > b;  // Greater than for descending
    });
    
    for (int n : numbers) cout << n << " ";
    cout << endl;
    // Output: 9 8 5 3 2 1
    
    return 0;
}
Finding Elements

Use lambdas with find_if, count_if, and similar algorithms to search based on custom conditions:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // Find first even number greater than 5
    auto it = find_if(numbers.begin(), numbers.end(), [](int n) {
        return n > 5 && n % 2 == 0;
    });
    
    if (it != numbers.end()) {
        cout << "Found: " << *it << endl;  // Output: Found: 6
    }
    
    // Count numbers divisible by 3
    int count = count_if(numbers.begin(), numbers.end(), [](int n) {
        return n % 3 == 0;
    });
    cout << "Divisible by 3: " << count << endl;  // Output: Divisible by 3: 3
    
    // Check if all elements are positive
    bool allPositive = all_of(numbers.begin(), numbers.end(), [](int n) {
        return n > 0;
    });
    cout << "All positive: " << boolalpha << allPositive << endl;
    // Output: All positive: true
    
    return 0;
}
Transforming Data

The transform algorithm combined with lambdas is a powerful way to modify or convert data:

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

int main() {
    // Square all numbers
    vector<int> numbers = {1, 2, 3, 4, 5};
    vector<int> squares(numbers.size());
    
    transform(numbers.begin(), numbers.end(), squares.begin(), [](int n) {
        return n * n;
    });
    
    for (int s : squares) cout << s << " ";
    cout << endl;
    // Output: 1 4 9 16 25
    
    // Convert strings to uppercase
    vector<string> words = {"hello", "world"};
    
    transform(words.begin(), words.end(), words.begin(), [](string s) {
        transform(s.begin(), s.end(), s.begin(), ::toupper);
        return s;
    });
    
    for (const auto& w : words) cout << w << " ";
    cout << endl;
    // Output: HELLO WORLD
    
    return 0;
}
Filtering with remove_if

Use lambdas with remove_if (combined with erase) to filter elements:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // Remove all odd numbers (erase-remove idiom)
    numbers.erase(
        remove_if(numbers.begin(), numbers.end(), [](int n) {
            return n % 2 != 0;  // Remove if odd
        }),
        numbers.end()
    );
    
    for (int n : numbers) cout << n << " ";
    cout << endl;
    // Output: 2 4 6 8 10
    
    // With capture: remove numbers below threshold
    int threshold = 5;
    vector<int> data = {1, 8, 3, 9, 2, 7, 4, 6};
    
    data.erase(
        remove_if(data.begin(), data.end(), [threshold](int n) {
            return n < threshold;
        }),
        data.end()
    );
    
    for (int n : data) cout << n << " ";
    cout << endl;
    // Output: 8 9 7 6
    
    return 0;
}
for_each and Accumulate

Lambdas work great with for_each for applying operations and accumulate for custom reductions:

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;

int main() {
    vector<int> numbers = {1, 2, 3, 4, 5};
    
    // for_each to print with formatting
    cout << "Numbers: ";
    for_each(numbers.begin(), numbers.end(), [](int n) {
        cout << "[" << n << "] ";
    });
    cout << endl;
    // Output: Numbers: [1] [2] [3] [4] [5]
    
    // accumulate with custom operation - product
    int product = accumulate(numbers.begin(), numbers.end(), 1, [](int acc, int n) {
        return acc * n;
    });
    cout << "Product: " << product << endl;  // Output: Product: 120
    
    // accumulate to find max (alternative to std::max_element)
    int maxVal = accumulate(numbers.begin(), numbers.end(), numbers[0], 
        [](int current_max, int n) {
            return (n > current_max) ? n : current_max;
        });
    cout << "Max: " << maxVal << endl;  // Output: Max: 5
    
    return 0;
}

Practice Questions: Lambdas with STL

Task: Given a vector of pairs (product name, price), sort them by price ascending, and if prices are equal, by name alphabetically.

Show Solution
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

int main() {
    vector<pair<string, double>> products = {
        {"Apple", 1.50},
        {"Banana", 0.75},
        {"Cherry", 2.00},
        {"Apricot", 1.50}  // Same price as Apple
    };
    
    sort(products.begin(), products.end(), 
        [](const auto& a, const auto& b) {
            if (a.second != b.second) {
                return a.second < b.second;  // Sort by price
            }
            return a.first < b.first;  // Then by name
        });
    
    for (const auto& p : products) {
        cout << p.first << ": $" << p.second << endl;
    }
    // Output:
    // Banana: $0.75
    // Apple: $1.5
    // Apricot: $1.5
    // Cherry: $2
    
    return 0;
}

Task: Given a vector of integers, create a new vector containing the squares of only the even numbers. Use copy_if and transform or a single pass approach.

Show Solution
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    vector<int> result;
    
    // Method 1: Using for_each with capture
    for_each(numbers.begin(), numbers.end(), [&result](int n) {
        if (n % 2 == 0) {
            result.push_back(n * n);
        }
    });
    
    cout << "Squares of even numbers: ";
    for (int n : result) cout << n << " ";
    cout << endl;
    // Output: Squares of even numbers: 4 16 36 64 100
    
    // Method 2: Copy evens first, then transform
    vector<int> evens;
    copy_if(numbers.begin(), numbers.end(), back_inserter(evens),
        [](int n) { return n % 2 == 0; });
    
    transform(evens.begin(), evens.end(), evens.begin(),
        [](int n) { return n * n; });
    
    cout << "Same result: ";
    for (int n : evens) cout << n << " ";
    cout << endl;
    
    return 0;
}

Key Takeaways

Lambda Syntax

Lambdas use [capture](params) -> return { body } syntax. Capture and body are required.

Capture Modes

Use [=] for value capture, [&] for reference. Mix with [=, &x] or [&, x] for specific needs.

Mutable Keyword

Add mutable after parameters to modify captured-by-value variables inside the lambda.

Generic Lambdas

Use auto parameters (C++14) to create lambdas that work with any type.

STL Integration

Lambdas excel with STL algorithms like sort, find_if, transform, and for_each.

Lifetime Caution

Avoid reference captures when lambda outlives the captured variable to prevent dangling references.

Knowledge Check

Test your understanding of C++ lambda expressions with these questions.
1 What does the capture clause [&] do in a lambda?
2 When do you need the mutable keyword in a lambda?
3 What is a generic lambda in C++14?
4 What is a potential danger of capturing by reference?
5 Which STL algorithm is commonly used with lambdas for custom sorting?
6 What is the output of: auto f = [x = 5]() mutable { return ++x; }; cout << f() << f();
Answer all questions to check your score