Appearance
Lambdas
Video: C++ Lambdas Part 1 - Unnamed function objects (closures) in C++ | Modern Cpp Series Ep. 100!!!
Lambdas allow you to create anonymous function objects. They provide a concise way to define functions inline while capturing variables from their surrounding scope.
They are essentially syntactic sugar for creating a class with an operator() method.
Basic syntax:
cpp
[capture-list](parameters) -> return-type { body }Simple example:
cpp
auto add = (int a, int b) { return a + b; };
int result = add(5, 3); // result = 8Lambda Components
1. Capture List [capture-list]
The capture list determines which variables from the enclosing scope are accessible inside the lambda:
cpp
int multiplier = 10;
auto multiply = [multiplier](int x) { return x * multiplier; };
int result = multiply(5); // result = 502. Parameter List (parameters)
Like regular functions, lambdas can take parameters:
cpp
auto compare = (int a, int b) { return a < b; };
bool isLess = compare(3, 7); // isLess = true3. Return Type -> return-type
The return type can be explicitly specified or deduced:
cpp
auto getLength = (const std::string& str) -> size_t { return str.length(); };
auto getLengthAuto = (const std::string& str) { return str.length(); }; // auto-deduced4. Body { body }
The function body contains the actual logic:
cpp
auto process = (int x) {
if (x > 0) return x * 2;
else return -x;
};Capture Modes
Value Capture [=]
Captures all variables by value (creates copies):
cpp
int x = 10;
int y = 20;
auto lambda = [=]() { return x + y; }; // Captures x and y by value
x = 100; // Changes original x, but lambda still uses copy (10)
int result = lambda(); // result = 30 (10 + 20)Reference Capture [&]
Captures all variables by reference:
cpp
int x = 10;
int y = 20;
auto lambda = [&]() { return x + y; }; // Captures x and y by reference
x = 100; // Changes original x
int result = lambda(); // result = 120 (100 + 20)Mixed Capture
Capture specific variables with different modes:
cpp
int x = 10;
int y = 20;
int z = 30;
auto lambda = [x, &y, z]() { return x + y + z; }; // x by value, y by reference, z by value
y = 100; // Changes original y
int result = lambda(); // result = 140 (10 + 100 + 30)Capture by Value with Mutable
Allows modification of captured values:
cpp
int counter = 0;
auto increment = [counter]() mutable { return ++counter; };
int result1 = increment(); // result1 = 1
int result2 = increment(); // result2 = 2
// Original counter is still 0Remember, lambdas without mutable capture cannot modify captured values that have been captured by value!
Lambda Implementation Internals
What the Compiler Actually Generates
When you write a lambda, the compiler generates a class (often called a "closure type") behind the scenes:
cpp
// Your lambda:
auto lambda = [x, &z](int y) { return x + y + z; };
// Compiler generates something like:
class __lambda_1 {
private:
const int x; // Captured by value; immutable in non-mutable lambda
int& z; // Captured by reference; refers to original variable
public:
__lambda_1(int x, int& z) : x(x), z(z) {} // Constructor binds reference
int operator()(int y) const { // Function call operator
return x + y + z;
}
};
auto lambda = __lambda_1(x, z); // Create instance, passing z by referenceMutable Lambda Implementation
cpp
int counter = 0;
auto lambda = [counter]() mutable { return ++counter; };
// Compiler generates:
class __lambda_4 {
private:
int counter; // Mutable copy
public:
__lambda_4(int c) : counter(c) {}
int operator()() { // Note: not const
return ++counter; // Can modify the copy
}
};As you can see, the compiler generates a class with a random name and a constructor that captures the captured variables. The function call operator is also generated. This is the reason why lambdas are called syntactic sugar, since they are essentially a syntactic shortcut for creating a class with an operator() method.
Lambda Types and Storage
Lambda Types are Unique
Each lambda expression creates a unique, unnamed type:
cpp
auto lambda1 = () { return 42; };
auto lambda2 = () { return 42; };
// lambda1 and lambda2 are different types!
// But you can use std::function for type erasure:
std::function<int()> func1 = lambda1;
std::function<int()> func2 = lambda2;Lambda Size Depends on Captures
cpp
auto lambda1 = () { return 42; }; // No captures
auto lambda2 = [x]() { return x; }; // Captures int by value
std::cout << sizeof(lambda1) << std::endl; // Usually 1 (empty class optimization)
std::cout << sizeof(lambda2) << std::endl; // Size of int (usually 4)Lambdas in Unevaluated Contexts
What are Unevaluated Contexts?
Unevaluated contexts are expressions that are not executed at runtime but are used for compile-time operations like:
sizeof()typeid()decltype()- Template parameters
noexcept()specifications
Lambdas work perfectly in unevaluated contexts like decltype() and template parameters. This is how libraries like Boost.Hana do their compile-time magic.
cpp
// Get lambda type without creating an instance
using lambda_type = decltype((int x) { return x * 2; });
// Use in templates (C++20)
template<auto Lambda>
void apply() {
constexpr auto result = Lambda(42);
}
apply<(int x) { return x * 2; }>();Boost.Hana: The Building Blocks
Boost.Hana's power comes from a simple but profound idea: lambdas can take types as parameters and return new types through decltype. Here's how the building blocks work:
Type as Parameter, Type as Return
To enable value semantics over types, we create a struct that just wraps a single type so that an instance of the struct can be passed to a lambda.
cpp
template<typename T>
struct type_c
{
using type = T;
};
// Lambda that takes two types and returns a new type
auto combine_types = (auto t1, auto t2) {
return std::pair<typename decltype(t1)::type, typename decltype(t2)::type>{};
};
// The decltype of the lambda call gives us the new type
using result_type = decltype(combine_types(
type_c<int>{},
type_c<double>{}
));
// result_type is std::pair<int, double>The key insight: the lambda is never executed. The compiler analyzes the decltype of the lambda call and extracts the return type, which becomes our new type. This lets us write type transformations that look like regular function calls. In fact, hana uses this to have very interesting type transformations that look like regular function calls.
cpp
template <typename V, V v, typename U, U u>
consteval auto operator+(std::integral_constant<V, v>, std::integral_constant<U, u>)
{
return integral_constant<decltype(v + u), v + u>{};
}
auto one = integral_constant<int, 1>{};
auto two = integral_constant<int, 2>{};
auto three = one + two; // three is integral_constant<int, 3>Hana goes one step further, and makes the creation of integral_constant easier using a literal _c. This simplifies the above example to:
cpp
auto three = 1_c + 2_c; // 1_c creates integral_constant<int, 1>, 2_c creates integral_constant<int, 2>Practical Examples
Lambda with Algorithm
cpp
std::vector<int> numbers = {1, 2, 3, 4, 5};
int threshold = 3;
// Count numbers greater than threshold
auto count = std::count_if(numbers.begin(), numbers.end(),
[threshold](int n) { return n > threshold; });
// Transform numbers using lambda
std::transform(numbers.begin(), numbers.end(), numbers.begin(),
(int n) { return n * n; });Lambda with Custom Comparator
cpp
std::vector<std::string> words = {"hello", "world", "cpp", "lambda"};
// Sort by length
std::sort(words.begin(), words.end(),
(const std::string& a, const std::string& b) {
return a.length() < b.length();
});Lambda with State
cpp
int sum = 0;
auto accumulator = [&sum](int x) { sum += x; };
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), accumulator);
// sum now contains 15Best Practices
1. Prefer Value Capture for Small Objects
cpp
// Good: Small objects by value
auto lambda = [x, y](int z) { return x + y + z; };
// Avoid: Large objects by value
auto lambda = [large_vector](int x) { /* ... */ }; // Expensive copy2. Use Reference Capture Carefully
cpp
int x = 42;
auto lambda = [&x]() { return x; };
// x must remain valid for the lifetime of lambda3. Consider Lambda Lifetime
cpp
std::function<int()> createLambda() {
int local = 42;
return [&local]() { return local; }; // DANGEROUS: local goes out of scope
}
// Better approach:
std::function<int()> createLambda() {
int local = 42;
return [local]() { return local; }; // Safe: captures by value
}4. Use mutable Sparingly
cpp
// Only use mutable when you need to modify captured values
int counter = 0;
auto lambda = [counter]() mutable { return ++counter; };Your Task
Implement a lambda factory pattern for the following problem:
Create a lambda that acts as a factory function, returning different operation functions based on a string parameter. The factory should return functions that either add, subtract, or multiply by a captured value.
cpp
// Example usage:
auto addFive = createOperation("add", 5);
int result = addFive(3); // result = 8 (5 + 3)
auto multiplyByTen = createOperation("multiply", 10);
int result2 = multiplyByTen(6); // result2 = 60 (10 * 6)Your solution should demonstrate:
- Lambda factories (functions that return other functions)
- Value capture in returned lambdas
- Conditional logic to return different lambda types
- Function composition and chaining
The createOperation function should support three operations:
- "add": Returns a function that adds the captured value to its argument
- "subtract": Returns a function that subtracts the captured value from its argument
- "multiply": Returns a function that multiplies the captured value by its argument
Create a lambda that acts as a factory function, returning different function objects based on a parameter. The factory should return functions that either add, subtract, or multiply by a captured value.
cpp
#include <string>
#include <functional>
// TODO: Implement createOperation function that acts as a lambda factory
// The function should take an operation string and a value, then return
// a function that performs the specified operation with the captured value
// TODO: Support three operations: "add", "subtract", "multiply"
// Each should return a lambda that captures the first operand and takes a second operand
// For example: createOperation("add", 5) should return a function allows people to add 5 to any integer.
// i.e. createOperation("add", 5)(3) should return 8.
auto createOperation = //LAMBDA_HERE;