C++ Learning Manual – New Features 46 – Lambda Expressions

In the world of C++ programming, we have always pursued more concise, flexible, and powerful expressions. The introduction of Lambda expressions is one of the significant gifts brought to us by C++11. It has fundamentally changed the way we define and use function objects, making the code more elegant and efficient.

What are Lambda Expressions?

Lambda expressions are a way to define anonymous functions introduced in C++11, allowing us to define functions directly where they are needed without naming them separately. This feature is particularly useful in scenarios involving callback functions, algorithm parameters, and more.

In simple terms, a Lambda expression is a function that is defined and used immediately; it can be created anywhere in the code and can capture variables from its surrounding scope.

Basic Syntax Structure

The basic syntax of a complete Lambda expression is as follows:

[capture list](parameter list) mutable (optional) exception specification (optional) -> return type (optional) {
    // function body
}

Let’s understand this with a simple example:

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

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // Using Lambda expression to filter even numbers
    auto even_numbers = numbers | std::views::filter([](int n) {
        return n % 2 == 0;
    });
    
    for (auto num : even_numbers) {
        std::cout << num << " ";
    }
    // Output: 2 4 6 8 10
    
    return 0;
}

Detailed Explanation of Capture List

The capture list is one of the most powerful features of Lambda expressions, allowing the Lambda to access variables from its surrounding scope.

Value Capture vs Reference Capture
#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    
    // Value capture
    auto lambda1 = [x]() {
        std::cout << "Value capture: " << x << std::endl;
        // x++; // Error: x is read-only
    };
    
    // Reference capture
    auto lambda2 = [&y]() {
        std::cout << "Reference capture: " << y << std::endl;
        y++; // Can modify the original variable
    };
    
    lambda1(); // Output: Value capture: 10
    lambda2(); // Output: Reference capture: 20
    std::cout << "Modified y: " << y << std::endl; // Output: Modified y: 21
    
    return 0;
}
Implicit Capture
#include <iostream>

int main() {
    int a = 5, b = 10, c = 15;
    
    // Implicit value capture of all variables
    auto lambda1 = [=]() {
        std::cout << a << ", " << b << ", " << c << std::endl;
    };
    
    // Implicit reference capture of all variables
    auto lambda2 = [&]() {
        a++; b++; c++;
        std::cout << a << ", " << b << ", " << c << std::endl;
    };
    
    // Mixed capture: value capture a, reference capture others
    auto lambda3 = [=, &b]() {
        // a is read-only, b can be modified
        b++;
        std::cout << a << ", " << b << std::endl;
    };
    
    lambda1(); // Output: 5, 10, 15
    lambda2(); // Output: 6, 11, 16
    lambda3(); // Output: 6, 12
    
    return 0;
}

mutable Keyword

By default, variables captured by value in the Lambda body are const; using mutable can remove this constness:

#include <iostream>

int main() {
    int count = 0;
    
    // Using mutable keyword
    auto counter = [count]() mutable {
        count++;
        std::cout << "Count: " << count << std::endl;
        return count;
    };
    
    counter(); // Output: Count: 1
    counter(); // Output: Count: 2
    counter(); // Output: Count: 3
    
    std::cout << "Original count: " << count << std::endl; // Output: Original count: 0
    
    return 0;
}

Return Type Deduction

Lambda expressions can automatically deduce the return type or specify it explicitly:

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

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    // Automatic return type deduction
    auto square = [](int x) { return x * x; };
    
    // Explicitly specifying return type
    auto safe_divide = [](double a, double b) -> double {
        if (b == 0) return 0;
        return a / b;
    };
    
    for (int num : numbers) {
        std::cout << square(num) << " ";
    }
    // Output: 1 4 9 16 25
    
    std::cout << std::endl << safe_divide(10, 3) << std::endl; // Output: 3.33333
    
    return 0;
}

Generic Lambda (C++14)

C++14 introduced generic Lambda, allowing the use of auto as parameter types:

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

int main() {
    // Generic Lambda - can handle any type that supports + operation
    auto add = [](auto a, auto b) {
        return a + b;
    };
    
    std::cout << add(10, 20) << std::endl;          // Output: 30
    std::cout << add(3.14, 2.71) << std::endl;      // Output: 5.85
    std::cout << add(std::string("Hello"), 
                    std::string(" World")) << std::endl; // Output: Hello World
    
    return 0;
}

Application in STL Algorithms

Lambda expressions combine perfectly with STL algorithms, greatly enhancing code readability and conciseness:

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

int main() {
    std::vector<int> numbers = {5, 2, 8, 1, 9, 3, 7, 4, 6};
    
    // Sorting - descending order
    std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
        return a > b;
    });
    
    std::cout << "Descending order: ";
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    
    // Finding the first element greater than 5
    auto it = std::find_if(numbers.begin(), numbers.end(), [](int n) {
        return n > 5;
    });
    
    if (it != numbers.end()) {
        std::cout << "First element greater than 5: " << *it << std::endl;
    }
    
    // Calculating average
    double sum = std::accumulate(numbers.begin(), numbers.end(), 0);
    double average = sum / numbers.size();
    
    // Counting elements greater than average
    int count = std::count_if(numbers.begin(), numbers.end(), [average](int n) {
        return n > average;
    });
    
    std::cout << "Average: " << average << std::endl;
    std::cout << "Number of elements greater than average: " << count << std::endl;
    
    return 0;
}

Capture *this (C++17)

C++17 allows capturing *this by value or reference:

#include <iostream>

class Processor {
private:
    int base_value;
public:
    Processor(int value) : base_value(value) {}
    
    auto create_multiplier() {
        // Capture *this by value
        return [*this](int factor) {
            return base_value * factor;
        };
    }
};

int main() {
    Processor processor(10);
    auto multiplier = processor.create_multiplier();
    
    std::cout << "Result: " << multiplier(5) << std::endl; // Output: 50
    
    return 0;
}

Template Lambda (C++20)

C++20 further enhances Lambda, supporting template parameters:

#include <iostream>
#include <vector>
#include <list>

int main() {
    // Template Lambda
    auto print_container = []<typename T>(const T& container) {
        for (const auto& item : container) {
            std::cout << item << " ";
        }
        std::cout << std::endl;
    };
    
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::list<std::string> lst = {"A", "B", "C", "D"};
    
    print_container(vec); // Output: 1 2 3 4 5
    print_container(lst); // Output: A B C D
    
    return 0;
}

Practical Application Scenarios

1. Event Handling
#include <iostream>
#include <functional>
#include <vector>

class Button {
private:
    std::vector<std::function<void()>> click_handlers;
public:
    void add_click_handler(std::function<void()> handler) {
        click_handlers.push_back(handler);
    }
    
    void click() {
        for (auto& handler : click_handlers) {
            handler();
        }
    }
};

int main() {
    Button button;
    int click_count = 0;
    
    // Adding Lambda as event handler
    button.add_click_handler([&click_count]() {
        click_count++;
        std::cout << "Button clicked! Count: " << click_count << std::endl;
    });
    
    button.click(); // Output: Button clicked! Count: 1
    button.click(); // Output: Button clicked! Count: 2
    
    return 0;
}
2. Asynchronous Programming
#include <iostream>
#include <future>
#include <thread>
#include <chrono>

int main() {
    // Using Lambda to start an asynchronous task
    auto future = std::async(std::launch::async, []() {
        std::this_thread::sleep_for(std::chrono::seconds(2));
        return "Asynchronous task completed!";
    });
    
    std::cout << "Main thread continues executing..." << std::endl;
    
    // Waiting for the asynchronous task to complete
    std::string result = future.get();
    std::cout << result << std::endl; // Output: Asynchronous task completed!
    
    return 0;
}

Best Practices and Considerations

  1. Avoid excessive use of reference capture: especially capturing variables with a short lifecycle
  2. Prefer value capture: unless modification of external variables is necessary
  3. Be aware of performance impacts: simple Lambdas are often inlined by the compiler
  4. Keep Lambdas short: complex logic should be placed in named functions
// Bad practice: overly complex Lambda
auto complex_lambda = [](const std::vector<int>& data) {
    // Contains a lot of complex logic...
    // Should be extracted to a separate function
};

// Good practice: concise Lambda
auto simple_lambda = [](int x) { return x * x; };

Conclusion

Lambda expressions are an indispensable feature in modern C++ programming, providing us with:

  • More concise code: reducing boilerplate and improving readability
  • More flexible programming: easily creating anonymous function objects
  • Better performance: compilers can optimize better
  • Stronger expressiveness: perfectly combining with STL algorithms

From C++11 to C++20, Lambda expressions have evolved, becoming more powerful and easier to use. Mastering Lambda expressions will make your C++ code more modern and elegant.

Leave a Comment