Lambda Function in C++

Lambda functions, also known as anonymous functions or closures, were introduced in C++11 as a way to define inline functions with concise syntax. They allow you to write small, function-like constructs in place without needing to define a separate function. Lambda functions are particularly useful in functional programming paradigms, especially when used with standard library algorithms like std::for_each, std::transform, or even threading APIs.

Syntax of Lambda Functions

[capture](parameters) -> return_type {
    // Function body
};
    • Capture: Defines how variables from the surrounding scope are captured and used inside the lambda function.
    • Parameters: Specifies the arguments passed to the lambda.
    • Return Type (optional): Specifies the type of the value returned by the lambda.
    • Body: The function body contains the actual code to be executed.

    Let’s understand this with simple example:

    #include <iostream>
    
    int main() {
        auto greet = []() {
            std::cout << "Hello, World!" << std::endl;
        };
        greet(); // Call the lambda function
    }
    

    Here, greet is a lambda function that takes no parameters and returns void. The lambda is assigned to the variable greet, which can then be called like a regular function.

    Capture Clause

    The capture clause allows a lambda to capture variables from the surrounding scope. There are two main ways to capture variables: by value ([=]) and by reference ([&]).

    1. Capture by Value ([=]): When capturing by value, the lambda function makes a copy of the variable. Any modifications to the captured variable inside the lambda do not affect the original variable

      #include <iostream>
      
      int main() {
          int x = 10;
          auto lambda = [=]() {
              std::cout << "Captured by value: " << x << std::endl;
          };
          lambda();
          x = 20; // Changing x after lambda capture has no effect on lambda
          lambda();
      }
      
      Output:
      Captured by value: 10
      Captured by value: 10
      

        2. Capture by Reference ([&]): When capturing by reference, the lambda function refers to the original variable. Any modifications to the variable inside the lambda will affect the original variable in the outer scope.

        #include <iostream>
        
        int main() {
            int x = 10;
            auto lambda = [&]() {
                std::cout << "Captured by reference: " << x << std::endl;
            };
            lambda();
            x = 20; // Changing x after lambda capture affects the lambda
            lambda();
        }
        
        Output:
        Captured by reference: 10
        Captured by reference: 20
        

        3. Mixed Capture You can mix capturing by value and reference. For example, you can capture some variables by value and others by reference

        int x = 10, y = 20;
        auto lambda = [x, &y]() {
            std::cout << "x (by value): " << x << std::endl;
            std::cout << "y (by reference): " << y << std::endl;
        };
        

        Parameters and Return Type

        Lambda functions can take parameters just like regular functions. If the lambda needs to return a value, you can specify the return type after the ->. However, C++ usually deduces the return type automatically if not specified.

        auto add = [](int a, int b) -> int {
            return a + b;
        };
        
        std::cout << add(3, 4); // Output: 7
        

        In many cases, you can omit the return type, as the compiler can deduce it:

        auto add = [](int a, int b) {
            return a + b;  // Compiler deduces the return type as int
        };
        

        Using Lambdas with Standard Algorithms

        Lambda functions are often used with STL algorithms like std::for_each, std::sort, and others. For example, you can use a lambda to sort a vector of integers:

        #include <algorithm>
        #include <iostream>
        #include <vector>
        
        int main() {
            std::vector<int> numbers = {5, 3, 1, 4, 2};
        
            // Sort using a lambda
            std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
                return a < b;
            });
        
            // Print the sorted vector
            for (int num : numbers) {
                std::cout << num << " ";
            }
            return 0;
        }
        

        In this example, the lambda defines a custom sorting rule that compares two integers and returns true if a is less than b.

        Lambda with Mutable Keyword

        By default, variables captured by value in a lambda are treated as const. To modify them inside the lambda, you need to use the mutable keyword.

        #include <iostream>
        
        int main() {
            int x = 10;
        
            auto lambda = [x]() mutable {
                x = 20;  // Modify the captured value
                std::cout << "x inside lambda: " << x << std::endl;
            };
        
            lambda();
            std::cout << "x outside lambda: " << x << std::endl;
        }
        
        Output:
        x inside lambda: 20
        x outside lambda: 10
        

        Without the mutable keyword, attempting to modify x inside the lambda would result in a compilation error.

        Let’s walk through an object-oriented programming (OOP) example that demonstrates the use of lambda functions. In this example, we’ll create a class that manages a collection of tasks (similar to a task scheduler) and use lambda functions to define actions for each task. We’ll showcase how lambda functions can be integrated into the design of a class in an OOP-friendly way.

        #include <iostream>
        #include <vector>
        #include <functional> // For std::function
        
        // TaskManager class that manages and executes tasks
        class TaskManager {
        public:
            // Add a new task with a lambda function
            void addTask(const std::function<void()>& task) {
                tasks.push_back(task);
            }
        
            // Execute all the stored tasks
            void executeAllTasks() {
                for (const auto& task : tasks) {
                    task(); // Invoke the lambda function
                }
            }
        
        private:
            // Vector to store tasks (lambda functions)
            std::vector<std::function<void()>> tasks;
        };
        

        int main() {
            TaskManager taskManager;
        
            // Task 1: Simple task to print a message
            taskManager.addTask([]() {
                std::cout << "Executing Task 1: Simple print task." << std::endl;
            });
        
            // Task 2: Capture a local variable by value
            int x = 10;
            taskManager.addTask([x]() {
                std::cout << "Executing Task 2: Captured value of x is " << x << std::endl;
            });
        
            // Task 3: Capture a local variable by reference
            int y = 20;
            taskManager.addTask([&y]() {
                y += 10;
                std::cout << "Executing Task 3: Modified value of y is " << y << std::endl;
            });
        
            // Task 4: Task using mutable lambda to modify captured values by value
            int z = 5;
            taskManager.addTask([z]() mutable {
                z *= 2; // Allowed due to mutable keyword
                std::cout << "Executing Task 4: Doubled value of z is " << z << std::endl;
            });
        
            // Execute all tasks
            std::cout << "Executing all tasks:" << std::endl;
            taskManager.executeAllTasks();
        
            // Check final value of y (which was captured by reference)
            std::cout << "Final value of y after all tasks: " << y << std::endl;
        
            return 0;
        }
        

        Explanation:
        1. Task 1: A simple lambda that prints a message. This demonstrates the most basic form of a lambda function with no captures.
        2. Task 2: A lambda function that captures the local variable x by value ([x]). The captured value of x remains constant inside the lambda, so even if x changes outside the lambda, the lambda retains its initial value.
        3. Task 3: A lambda function that captures the local variable y by reference ([&y]). Here, the lambda modifies y within its body, and since y is captured by reference, the modification is reflected in the original variable outside the lambda.
        4. Task 4: A lambda that uses the mutable keyword to modify the value of z, even though it was captured by value ([z]). Normally, variables captured by value are const inside the lambda, but mutable allows modifying the copy of the captured variable.
        5. OOP Integration: The TaskManager class integrates lambda functions with an object-oriented design. This pattern demonstrates how you can encapsulate the behavior (i.e., tasks) as lambda functions and store them for later execution. The use of lambdas makes the code concise and flexible, especially when tasks are small and can be defined in place.