Lambda Expressions
Note: Considering the practical context of competitive programming, this article will not comprehensively study grammar but will only cover parts that may be applicable in competitive programming.
This article's grammar references the C++11 standard. Grammar from higher standards will be mentioned as appropriate and specially noted.
Lambda Expressions
Lambda expressions are named after λ-calculus in mathematics, directly corresponding to lambda abstraction. During compilation, the compiler generates an anonymous function object based on the syntax, with captured variables as its members, and parameters and function body used to implement the operator() overload.
Function Object
A function object is a class object, typically implemented by overloading operator(), allowing it to be called like a function. Compared to using ordinary functions, function objects have many advantages, such as being able to preserve state and being passed as parameters to other functions.
The following is one syntax for lambda:
1 | |
A lambda expression itself is a class, which expands to the following form:
1 2 3 4 5 6 7 8 9 10 | |
An empty capture can be implicitly converted to a function pointer, for example:
1 | |
Below, we introduce each part of the syntax separately.
statement Function Body
The function body is similar to that of an ordinary function, except it can access not only parameters and global variables but also variables captured.
capture Clause
A lambda begins with a capture clause, which specifies which variables are captured. The capture list can be empty or specify the capture mode: variables prefixed with & are accessed by reference, and variables without this prefix are accessed by value.
We can also use default capture modes to capture all variables mentioned in the lambda: & means all captured variables are accessed by reference, = means all captured variables are accessed by value.
After a default capture, you can still explicitly specify the capture mode for particular variables.
If you need to access external variable a by reference and external variable b by value, the following capture clauses can all achieve this:
[&a, b][b, &a][&, b][b, &][=, &a]
The capture list can also be used to declare variables, with the type deduced by the initializer, similar to declaring variables with auto.
Here are some common examples:
1 2 3 4 5 6 7 8 9 10 | |
Generalized Capture with Initialization (C++14)
Since C++14, capture can not only be used to capture external variables but also to declare and initialize new variables, for example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
When declaring a new variable, the initial value cannot be omitted. The variable's type is determined by the type of the initial value, equivalent to:
1 | |
The following is an incorrect writing:
1 2 | |
The initializer can also be an external variable, for example:
1 2 3 | |
val can also be a reference type, referencing an external variable. This way, you can give an alias to an external variable captured by reference, for example:
1 2 3 4 5 6 7 8 9 10 11 | |
Capturing external variables and defining new variables can be used simultaneously.
If you want to modify a new variable defined in the capture within the lambda expression, you need to use the mutable keyword; if it's a reference, it's not needed, for example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
See mutable Specification for details.
The lifetime of variables defined in the capture follows the receiver of the lambda expression, which in the above examples is variable \(f\), because the lambda itself is actually a class, and everything in the capture are private member variables of this class, for example:
1 2 3 4 5 6 7 | |
parameters Parameter List
In most cases, it is similar to a function's parameter list, for example:
1 2 3 | |
This will print the array x sorted in descending order.
Since the parameters parameter list is optional, if no parameters are passed to the lambda, and its declaration does not include mutable, and there is no trailing return type, the empty parentheses can be omitted.
Parameters Declared with auto
Since C++14, if a parameter is declared with auto, a Generic Lambda Expression is constructed.
Explicit Object Parameter (C++23)
Since C++23, explicit object parameters can be used in lambda parameters.
1 2 3 4 5 | |
mutable Specification
Allows the function body to modify variables captured by value.
1 2 3 4 5 6 | |
After executing by_value(), the captured member a in by_value becomes 1, but the external variable a remains 0.
After executing by_ref(), the external a becomes 1.
return-type Return Type
Used to specify the return type of the lambda expression. If omitted, the return type will be automatically deduced (behaving like a function with auto declared return type).
Multiple return statements with inconsistent deduced types will cause a compilation error.
1 2 3 4 5 6 7 8 | |
Generic Lambda (C++14)
Using auto as a parameter type allows constructing a generic lambda.
1 | |
In cpp insights, you can observe the compiler-generated lambda class definition:
1 2 3 4 5 6 7 8 9 | |
Both parameters of add are declared with auto, corresponding to the two template parameters T and U of the operator() function template in the add_lambda class.
Recursion in Lambda
First, consider a compilation failure example:
1 2 3 4 5 6 7 8 9 | |
Here we attempt to capture \(dfs\) in the capture list, but there is a problem: the type of \(dfs\) is auto, which will be deduced only after the type on the right side of the equals sign is deduced. However, for the lambda to capture \(dfs\), it must first determine the type of \(dfs\) to create its reference variable. This creates a chicken-and-egg situation.
How to solve this problem?
-
Explicitly specify the type of \(dfs\), for example, using
std::function.Modify the above code to:
1 2 3 4 5 6 7 8 9 10
int n = 10; std::function<void(int)> dfs = [&](int i) -> void { if (i == n) return; else dfs(i + 1); // OK }; dfs(1);Not recommended to use
std::functionfor recursionThe type erasure of
std::functionusually requires allocating extra memory, and the indirect call overhead further reduces performance.In Benchmark tests using Clang 17 compiler and libc++ as the standard library, recursion implemented with
std::functionis about 2.5 times slower than lambda implementation.Test code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
#include <algorithm> #include <functional> #include <numeric> #include <random> using namespace std; const auto& nums = [] { random_device rd; mt19937 gen{rd()}; array<unsigned, 32> arr{}; std::iota(arr.begin(), arr.end(), 0u); ranges::shuffle(arr, gen); return arr; }(); static void std_function_fib(benchmark::State& state) { std::function<int(int)> fib; fib = [&](int n) { return n <= 2 ? 1 : fib(n - 1) + fib(n - 2); }; unsigned i = 0; for (auto _ : state) { auto res = fib(nums[i]); benchmark::DoNotOptimize(res); ++i; if (i == nums.size()) i = 0; } } BENCHMARK(std_function_fib); static void template_lambda_fib(benchmark::State& state) { auto n_fibonacci = [](const auto& self, int n) -> int { return n <= 2 ? 1 : self(self, n - 1) + self(self, n - 2); }; unsigned i = 0; for (auto _ : state) { auto res = n_fibonacci(n_fibonacci, nums[i]); benchmark::DoNotOptimize(res); ++i; if (i == nums.size()) i = 0; } } BENCHMARK(template_lambda_fib);- Instead of capturing \(dfs\), pass it as a function parameter.
Modify the above code to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
int n = 10; // If the parameter list contains a parameter of type auto, then the operator() // function in this Lambda class will be defined as a template function, which can be instantiated later when called auto dfs = [&](auto& self, int i) -> void // [&] only captures used variables, so it won't capture auto dfs { if (i == n) return; else self(self, i + 1); // OK }; dfs(dfs, 1);Difference between
auto self,auto& self, andauto&& self:Theoretically, both
auto& selfandauto&& selfwill only use \(8\) bytes (pointer size) for parameter passing, without additional copying. The actual behavior depends on the compiler's implementation of Lambda and corresponding optimizations. Usingauto selfwill cause object copying, and the copy size depends on the elements in the capture list, as they are all private member variables of this Lambda class.- Manually expand the Lambda class or use a similar approach, allowing direct declaration of \(dfs\)'s type.
Modify the above code to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
int n = 10; class Lambda_1 { public: auto operator()(int i) const -> void { if (i == n) return; else (*this)(i + 1); // OK } explicit Lambda_1(int& __n) : n(__n) {} private: int& n; } dfs(n); dfs(1);- If the lambda does not capture any variables, we can also utilize function pointers.
If a lambda does not capture any variables, it can be implicitly converted to a function pointer. At the same time, the lambda can also be declared as
static, and the function pointer type can also be declared asstatic. This way, the lambda can access the function pointer without capturing, enabling recursion.Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14
static unsigned (*fptr)(unsigned); static const auto lambda = [](const unsigned a) { return a < 2 ? a : (*fptr)(a - 2) + (*fptr)(a - 1); }; static auto init = [] { fptr = +lambda; // Or // fptr = static_cast<unsigned (*)(unsigned)>(lambda); return 0; }(); cout << lambda(10);
Applications of Lambda Expressions
As Predicate for Standard Library Algorithms
Sort in descending order:
1 2 | |
Use std::find_if to find the first element greater than 3:
1 2 | |
Controlling Lifetime of Intermediate Variables
In competitive programming, we encounter scenarios like: a variable's initialization requires previously declared variables, and its initialization process generates large intermediate variables occupying significant memory.
We hope to destruct these intermediate variables as soon as possible to reduce memory consumption. Here, we can use lambda to control the lifetime of these intermediate variables.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Compared to using block scope, lambda allows us to use return values, making the code more concise; compared to functions, we don't need to name and declare various captured parameters separately, making the code more compact.
References
本页面最近更新:,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:OI-wiki
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用