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:
- Task 1: A simple lambda that prints a message. This demonstrates the most basic form of a lambda function with no captures.
- Task 2: A lambda function that captures the local variable
x
by value ([x]
). The captured value ofx
remains constant inside the lambda, so even ifx
changes outside the lambda, the lambda retains its initial value. - Task 3: A lambda function that captures the local variable
y
by reference ([&y]
). Here, the lambda modifiesy
within its body, and sincey
is captured by reference, the modification is reflected in the original variable outside the lambda. - Task 4: A lambda that uses the
mutable
keyword to modify the value ofz
, even though it was captured by value ([z]
). Normally, variables captured by value areconst
inside the lambda, butmutable
allows modifying the copy of the captured variable. - 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.