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.
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;
}
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;
}
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;
}
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;
}
// 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;
}
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;
}
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.