Lambda Expressions and Function Objects
45 minLambda expressions (introduced in C++11) create anonymous function objects inline, making code more concise and expressive. Lambdas are particularly useful with STL algorithms, where custom behavior is needed without defining separate functions or function objects. A lambda consists of a capture list (specifying which variables from the enclosing scope to access), parameters, return type (often deduced), and a function body.
Lambda captures determine how variables from the enclosing scope are accessed. Capture by value [=] copies variables, while capture by reference [&] creates references. Individual variables can be captured specifically: [x, &y] captures x by value and y by reference. Mutable lambdas [mutable] allow captured-by-value variables to be modified within the lambda. Understanding captures is crucial for correct lambda behavior.
Function objects (functors) are classes that overload operator(), making instances callable like functions. Functors predate lambdas and are still useful for complex, reusable callable objects. Functors can maintain state between calls, making them more powerful than regular functions. Many STL algorithms accept function objects, and lambdas are essentially compiler-generated function objects.
std::function (C++11) provides a type-erased wrapper for any callable object (functions, lambdas, functors, member functions). std::function enables storing and passing callables with different types through a uniform interface. This is useful for callbacks, event handlers, and function tables. However, std::function has overhead compared to direct function calls or template parameters.
Lambda expressions support generic programming through generic lambdas (C++14), which use auto for parameter types. Generic lambdas are essentially templates that work with any compatible type. This enables writing highly reusable lambda code that works across different types without explicit template syntax. Generic lambdas make algorithms even more flexible and expressive.
Modern C++ best practices favor lambdas for short, inline callable code, especially with STL algorithms. Lambdas improve code readability by keeping related code together and eliminating the need for separate function definitions. For complex, reusable callable objects, function objects or std::function may still be appropriate. Understanding when to use each approach enables writing clean, efficient C++ code.
Key Concepts
- Lambda expressions create anonymous function objects inline.
- Captures determine how enclosing scope variables are accessed.
- Function objects (functors) provide reusable callable behavior.
- std::function provides type-erased callable wrappers.
- Generic lambdas (C++14) enable type-agnostic callable code.
Learning Objectives
Master
- Creating and using lambda expressions
- Understanding lambda captures (by value, by reference)
- Implementing function objects (functors)
- Using std::function for type-erased callables
Develop
- Understanding callable objects in C++
- Writing expressive, concise code with lambdas
- Choosing appropriate callable mechanisms
Tips
- Use lambdas with STL algorithms: std::for_each(vec.begin(), vec.end(), [](int x) {...});
- Capture specifically: [x, &y] instead of [=] or [&] when possible.
- Use mutable for modifying captured-by-value variables: [x]() mutable { x++; }.
- Prefer lambdas for short, inline code; functors for complex, reusable logic.
Common Pitfalls
- Capturing by reference when object lifetime is uncertain, causing dangling references.
- Overusing std::function when templates would work, adding unnecessary overhead.
- Not understanding capture semantics, causing unexpected behavior.
- Creating overly complex lambdas, reducing readability.
Summary
- Lambdas provide concise, inline function objects.
- Captures control access to enclosing scope variables.
- Function objects enable reusable, stateful callable behavior.
- std::function provides uniform interface for different callable types.
Exercise
Demonstrate lambda expressions and function objects.
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
// Function object (functor)
class Multiplier {
private:
int factor;
public:
Multiplier(int f) : factor(f) {}
int operator()(int x) const {
return x * factor;
}
};
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// Lambda expression
auto printNumber = [](int n) {
std::cout << n << " ";
};
std::cout << "Numbers: ";
std::for_each(numbers.begin(), numbers.end(), printNumber);
std::cout << std::endl;
// Lambda with capture
int threshold = 3;
auto countAbove = [threshold](const std::vector<int>& vec) {
return std::count_if(vec.begin(), vec.end(),
[threshold](int n) { return n > threshold; });
};
std::cout << "Numbers above " << threshold << ": "
<< countAbove(numbers) << std::endl;
// Function object usage
Multiplier multiplyBy2(2);
std::vector<int> doubled;
std::transform(numbers.begin(), numbers.end(),
std::back_inserter(doubled), multiplyBy2);
std::cout << "Doubled numbers: ";
for (int n : doubled) {
std::cout << n << " ";
}
std::cout << std::endl;
return 0;
}
Exercise Tips
- Try different captures: [=], [&], [x, &y], [this] for member access.
- Use generic lambdas: [](auto x, auto y) { return x + y; }.
- Create functors: class Multiplier { int operator()(int x) const { return x * factor; } };
- Store lambdas: std::function<int(int)> func = [](int x) { return x * 2; };.