cpp_033: C++ Lambda Expressions
Lambda expressions, introduced in C++11, are a powerful feature that allows you to define anonymous functions inline. They are particularly useful for writing concise, localized function-like behavior, often used with algorithms, callbacks, or custom logic. This post explores lambda syntax, capture mechanisms, practical examples, and a problem to test your understanding.
1. Lambda Syntax
The general syntax of a lambda expression in C++ is:
[capture](parameters) -> return_type {
// function body
};- Capture: Specifies variables from the surrounding scope to be used in the lambda.
- Parameters: Defines the input arguments, similar to a regular function.
- Return Type: Optional; specifies the return type. If omitted, the compiler infers it.
- Function Body: Contains the logic of the lambda.
Lambdas can be stored in variables (often using auto), passed to functions, or used directly.
2. Why Use Lambdas?
Lambdas are ideal for:
- Short, localized functions: Avoid defining separate named functions for simple tasks.
- Algorithm customization: Work seamlessly with STL algorithms like std::sort or std::for_each.
- Callbacks and event handling: Pass behavior to functions or classes.
- Closures: Capture and manipulate variables from the surrounding scope.
3. Basic Examples with Explanations
3.1. Basic Lambda with No Capture or Parameters
This lambda takes no arguments and performs a simple action.
#include <iostream>
using namespace std;
int main() {
auto greet = []() {
cout << "Hello from Lambda!\n";
};
greet(); // Calls the lambda
return 0;
}Explanation:
- The lambda []() has an empty capture list ([]) and no parameters (()).
- It’s stored in the greet variable (type deduced as auto).
- Calling greet() executes the lambda, printing “Hello from Lambda!”.
3.2. Lambda with Parameters
Lambdas can take parameters like regular functions.
#include <iostream>
using namespace std;
int main() {
auto add = [](int a, int b) {
return a + b;
};
cout << "3 + 4 = " << add(3, 4) << "\n"; // Outputs: 3 + 4 = 7
return 0;
}Explanation:
- The lambda [](int a, int b) accepts two integers and returns their sum.
- The return type is inferred as int because the expression a + b is an integer.
- The lambda is invoked with add(3, 4).
3.3. Lambda Capturing Variables
Lambdas can capture variables from their surrounding scope by value or reference.
#include <iostream>
using namespace std;
int main() {
int x = 5, y = 10;
auto showByValue = [x]() {
cout << "Captured by value: " << x << "\n";
};
auto showByRef = [&y]() {
cout << "Captured by reference: " << y << "\n";
};
showByValue(); // Outputs: Captured by value: 5
showByRef(); // Outputs: Captured by reference: 10
return 0;
}Explanation:
- [x]: Captures x by value, creating a copy inside the lambda. Changes to x outside don’t affect the lambda.
- [&y]: Captures y by reference, so the lambda uses the original y. Changes to y are reflected inside the lambda.
Capture modes:
- [=]: Capture all variables by value.
- [&]: Capture all variables by reference.
- [x, &y]: Mix specific captures (e.g., x by value, y by reference).
Get CodeAddict’s stories in your inbox
Join Medium for free to get updates from this writer.
Another Exam:
#include <iostream>
using namespace std;
int main() {
int a = 5, b = 10;
// [=] - capture all used variables by value
auto lambda1 = [=]() {
cout << "lambda1: a = " << a << ", b = " << b << endl;
};
// [&] - capture all used variables by reference
auto lambda2 = [&]() {
cout << "lambda2: a = " << a << ", b = " << b << endl;
};
// [a] - capture only a by value, b is not captured (error if used)
auto lambda3 = [a]() {
cout << "lambda3: a = " << a << endl;
// cout << "b = " << b << endl; // ❌ error: 'b' not captured
};
// [=, &b] - capture all by value, but b by reference
auto lambda4 = [=, &b]() {
cout << "lambda4: a = " << a << ", b = " << b << endl;
};
// [&, a] - capture all by reference, but a by value
auto lambda5 = [&, a]() {
cout << "lambda5: a = " << a << ", b = " << b << endl;
};
a = 100; b = 200;
lambda1(); // a and b were captured by value, so it shows 5, 10
lambda2(); // a and b by reference, shows 100, 200
lambda3(); // only a by value, shows 5
lambda4(); // a by value (5), b by reference (200)
lambda5(); // a by value (5), b by reference (200)
return 0;
}Output:
- Capture by Value ([=])
- The lambda makes a copy of the variable at the moment the lambda is defined.
- If the original variable is modified after the lambda is created, the lambda’s copy doesn’t change.
Capture by Reference ([&])
- The lambda uses the actual original variable, not a copy.
- If the original variable is modified later, the lambda sees the updated value.
3.4. Lambda with Explicit Return Type
You can specify the return type explicitly for clarity or when the compiler can’t infer it.
[captures](parameters) -> return_type {
// lambda body
}#include <iostream>
using namespace std;
int main() {
auto divide = [](double a, double b) -> double {
if (b == 0) return 0;
return a / b;
};
cout << "10 / 2 = " << divide(10, 2) << "\n"; // Outputs: 10 / 2 = 5
return 0;
}Explanation:
- The -> double explicitly declares the return type as double.
- This is useful when the lambda’s return type is ambiguous or needs to be enforced.
3.5. Using Lambda in a Loop
Lambdas are often used with containers and loops for concise iteration.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> numbers = {1, 2, 3, 4};
auto print = [](int value) {
cout << "Value: " << value << "\n";
};
for (int n : numbers)
print(n); // Outputs each number on a new line
return 0;
}Explanation:
- The lambda print takes a single parameter and prints it.
- It’s used in a range-based for loop to process each element in the vector.
- Alternatively, you could use for_each with the lambda for a more functional approach.
3.6. Lambda with Mutable Keyword
By default, captured variables by value are const in the lambda body. Use mutable to modify them.
#include <iostream>
using namespace std;
int main() {
int counter = 0;
auto increment = [counter]() mutable {
counter++;
cout << "Inside lambda counter: " << counter << "\n";
};
increment(); // Outputs: Inside lambda counter: 1
increment(); // Outputs: Inside lambda counter: 2
cout << "Outside lambda counter: " << counter << "\n"; // Outputs: Outside lambda counter: 0
return 0;
}Explanation:
- counter is captured by value, so the lambda has its own copy.
- The mutable keyword allows the lambda to modify this copy.
- The original counter remains unchanged, as shown by the final output.
3.7. Beginner C++ Lambda Problem and Solution
Problem
Write a C++ program that uses lambda expressions to process a vector of integers (numbers = {10, 20, 30, 40, 50}) and performs the following tasks:
- Print with Offset: Use a lambda to print each number in the vector, adding a constant offset (e.g., offset = 5) to each number. Capture the offset by value.
- Count Multiples: Use a lambda to count how many numbers in the vector are multiples of a divisor (e.g., divisor = 10). Capture the divisor by value and use an explicit return type.
- Track Calls: Use a lambda with the mutable keyword to track how many times it’s called while printing the numbers. Capture a counter by value and modify it inside the lambda.
- Modify by Reference: Use a lambda to double each number in the vector by capturing the numbers by reference. Print the modified vector.
Use using namespace std; and a vector<int> for the numbers. Print clear output for each task.
Solution
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> numbers = {10, 20, 30, 40, 50};
int offset = 5;
int divisor = 10;
int call_counter = 0;
// Task 1: Print with Offset
cout << "Task 1: Numbers with offset " << offset << ":\n";
auto printWithOffset = [offset](int value) {
cout << value + offset << " ";
};
for (int n : numbers) {
printWithOffset(n);
}
cout << "\n\n";
// Task 2: Count Multiples
auto countMultiples = [divisor](int value) -> bool {
return value % divisor == 0;
};
int multiple_count = 0;
for (int n : numbers) {
if (countMultiples(n)) {
multiple_count++;
}
}
cout << "Task 2: Count of numbers divisible by " << divisor << ": " << multiple_count << "\n\n";
// Task 3: Track Calls with Mutable
cout << "Task 3: Printing numbers with call tracking:\n";
auto printWithTracking = [call_counter](int value) mutable {
call_counter++;
cout << "Call " << call_counter << ": " << value << "\n";
};
for (int n : numbers) {
printWithTracking(n);
}
cout << "Original call_counter: " << call_counter << "\n\n";
// Task 4: Modify by Reference
cout << "Task 4: Doubling numbers (modify by reference):\n";
auto doubleValue = [&numbers](int index) {
numbers[index] *= 2;
};
for (int i = 0; i < numbers.size(); i++) {
doubleValue(i);
}
cout << "Modified numbers: ";
for (int n : numbers) {
cout << n << " ";
}
cout << "\n";
return 0;
}Output:
Task 1: Numbers with offset 5:
15 25 35 45 55
Task 2: Count of numbers divisible by 10: 5
Task 3: Printing numbers with call tracking:
Call 1: 10
Call 2: 20
Call 3: 30
Call 4: 40
Call 5: 50
Original call_counter: 0
Task 4: Doubling numbers (modify by reference):
Modified numbers: 20 40 60 80 100 






