-
Notifications
You must be signed in to change notification settings - Fork 15
Article on lambdas #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,365 @@ | ||||||
## Abstract | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
||||||
A lambda expression is a shorthand notation for creating an unnamed callable object (also called a closure, or an | ||||||
anonymous function). A lambda can "capture" variables from its surrounding scope by value or by reference, allowing the | ||||||
body of the lambda to access or modify those variables without having to pass them as parameters. Unlike regular | ||||||
functions, lambdas are typically written in-line, combining the reusability of functions with direct access to local | ||||||
context. The lambda retains captured variables' state (for captures by value) or dynamically references them (for | ||||||
captures by reference), making lambdas ideal for short, context-dependent operations like custom comparisons, filters, | ||||||
or event handlers. | ||||||
|
||||||
For example: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example comes after a couple paragraphs on captures, but doesn't use any captures. Maybe it'd be good to show this code example early on, then talk about captures, then show an example using captures? |
||||||
|
||||||
```cpp | ||||||
// Define a function in namespace scope | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm inclined to say this comment and others can be dropped, however, if anyone strongly feels this would be helpful for beginners reading the article then it's ok to keep. I think this article is a bit more of a deep-dive than beginner content, though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comments are meant to emphasize and explain the difference between the code snippets. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's pretty clear from just reading the code. Thoughts? |
||||||
bool is_even(int x) { | ||||||
return x % 2 == 0; | ||||||
} | ||||||
|
||||||
int main() { | ||||||
std::vector<int> data = { 1, 3, 5, 10, 73 }; | ||||||
|
||||||
// Actually use the function here | ||||||
bool has_even = std::ranges::any_of(data, is_even); | ||||||
} | ||||||
``` | ||||||
can instead be rewritten as: | ||||||
```cpp | ||||||
int main() { | ||||||
std::vector<int> data = { 1, 3, 5, 10, 73 }; | ||||||
// Define a lambda in-line | ||||||
bool has_even = std::ranges::any_of(data, [](int x) { return x % 2 == 0; }); | ||||||
} | ||||||
``` | ||||||
|
||||||
| ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don't try to manually insert space like this :) This will introduce inconsistency with how the site presents itself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I use this as a break between paragraphs. Should I use something else or avoid breaks entirely? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’d say avoid them entirely. The site styling already handled spacing between paragraphs and headers. |
||||||
|
||||||
| ||||||
|
||||||
## Syntax | ||||||
|
||||||
The basic syntax of a lambda expression looks like this: | ||||||
|
||||||
``` | ||||||
[captures](params) -> ReturnType { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. -_- |
||||||
statements; | ||||||
} | ||||||
``` | ||||||
Comment on lines
+46
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be marked as cpp source, even though it's pseudo-code |
||||||
|
||||||
A more detailed explanation of the syntax can be found on | ||||||
[cppreference](https://en.cppreference.com/w/cpp/language/lambda#Syntax). | ||||||
|
||||||
| ||||||
|
||||||
Each lambda expression has its own unique, unnameable type: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd recommend moving this part to a later section as it's an implementation detail that doesn't pertain to most uses of lambdas There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where should I move this to? Also, I think showing that a lambda is really similar to a struct with an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a later section on implementation details |
||||||
|
||||||
```cpp | ||||||
auto add = [](int x, int y) { return x + y; }; | ||||||
``` | ||||||
is similar to writing: | ||||||
```cpp | ||||||
struct { | ||||||
auto operator()(int x, int y) const { | ||||||
return x + y; | ||||||
} | ||||||
} add; | ||||||
``` | ||||||
|
||||||
This also means that even identical lambda expressions always have completely different types, and are not | ||||||
interconvertible: | ||||||
|
||||||
```cpp | ||||||
auto foo = [](int x) { return x + 1; }; | ||||||
auto bar = [](int x) { return x + 1; }; | ||||||
|
||||||
static_assert(not std::same_as<decltype(foo), decltype(bar)>); | ||||||
``` | ||||||
| ||||||
### Capture list | ||||||
All lambda expressions begin with a capture list. The capture list specifies which variables to capture from the | ||||||
surrounding scope: | ||||||
```cpp | ||||||
int x = 10; | ||||||
auto lambda = [x](int y) { return x + y; }; | ||||||
lambda(5) // 15 | ||||||
``` | ||||||
|
||||||
- `[]` - Empty capture list. | ||||||
- `[=]` - Automatically capture variables that are used in the lambda body by value. | ||||||
- Mutually exclusive with `[&]`. | ||||||
- Does not implicitly capture `this` if used in a class. | ||||||
- `[&]` - Automatically capture variables that are used in the lambda body by reference. | ||||||
- Mutually exclusive with `[=]`. | ||||||
- `[x]` - Capture only `x` by value. | ||||||
- `[&x]` - Capture only `x` by reference. | ||||||
- `[x...]` - Capture a pack `x` by value. | ||||||
- `[&x...]` - Capture a pack `x` by reference. | ||||||
Comment on lines
+106
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a cool bit of trivia and I had to go check the grammar about it. While this is technically its own special syntax in the standard, it's not different from a user perspective. Maybe these could be moved to a note along the lines of "Captures even work with packs!" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea |
||||||
- `[this]` - Capture `*this` by reference. | ||||||
- `[x = y]` - Define a local variable `x` and initialize it to `y`. | ||||||
|
||||||
Different captures can be mixed together: | ||||||
|
||||||
```cpp | ||||||
int a = 1; | ||||||
int b = 2; | ||||||
int c = 3; | ||||||
|
||||||
auto foo = [=, &b, d = c]() { | ||||||
// `a` is implicitly captured by value, | ||||||
// `b` is explicitly captured by reference, and | ||||||
// `d` is initialized to `c`. | ||||||
return a + b + d; | ||||||
}; | ||||||
``` | ||||||
|
||||||
However, `[=]` may only be followed by captures by reference and `[&]` may only be followed by captures by value. | ||||||
|
||||||
Implicit captures can be nested multiple times: | ||||||
|
||||||
```cpp | ||||||
int x = 0; | ||||||
|
||||||
[&]() { | ||||||
[&]() { | ||||||
x = 5; | ||||||
}(); | ||||||
}(); | ||||||
``` | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think another cool thing to mention in this section about captures would be that lambdas under the hood only capture what they actually use, so |
||||||
| ||||||
|
||||||
### Parameters | ||||||
|
||||||
Lambda parameters work the same way as normal function parameters, and `auto` parameters make a lambda's `operator()` | ||||||
implicitly templated. | ||||||
|
||||||
If a lambda takes no parameters, the parameter list may be omitted entirely: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd probably move this to the end of the section, just so things flow more naturally (discussion about parameters followed by an example of parameters instead of a discussion of parameters followed by an example of no parameters) |
||||||
|
||||||
```cpp | ||||||
auto very_important_number = [] { return 4; }; | ||||||
``` | ||||||
Finally, the explicit `this` parameter can also be in lambdas since C++23: | ||||||
```cpp | ||||||
auto fibonacci = [](this auto self, int n) { | ||||||
if (n < 2) return n; | ||||||
return self(n - 1) + self(n - 2); | ||||||
}; | ||||||
``` | ||||||
|
||||||
| ||||||
|
||||||
### Return type | ||||||
|
||||||
A lambda's return type may be specified with an arrow: | ||||||
|
||||||
```cpp | ||||||
auto add = [](int x, int y) -> int { return x + y; }; | ||||||
``` | ||||||
Omitting the return type is the same as writing `-> auto`. | ||||||
| ||||||
### Specifiers | ||||||
- `constexpr` - Explicitly specifies that a lambda's `operator()` is a | ||||||
[constexpr function](https://en.cppreference.com/w/cpp/language/constexpr#constexpr_function). | ||||||
- Mutually exclusive with `consteval`. | ||||||
- Lambdas are implicitly marked `constexpr`, if possible. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it'd be helpful to link to the criteria that determines if this is possible, either directly or as a footnote |
||||||
- `consteval` - Makes a lambda's `operator()` an | ||||||
[immediate function](https://en.cppreference.com/w/cpp/language/consteval). | ||||||
- Mutually exclusive with `constexpr`. | ||||||
- `static` - Makes a lambda's `operator()` a | ||||||
[static member function](https://en.cppreference.com/w/cpp/language/static#Static_member_functions). | ||||||
- Mutually exclusive with `mutable`. | ||||||
- Cannot be used if the captures list is not empty, or an explicit `this` parameter is present. | ||||||
Comment on lines
+185
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be helpful to describe why this might be useful. Also it might be worth noting the lambda -> function pointer trick in some other section, and how this works even without auto* ptr = +[]{ return 4; }; |
||||||
- `mutable` - Allows the body of the lambda to modify the variables captured by value. | ||||||
- Mutually exclusive with `static`. | ||||||
- Cannot be used if an explicit `this` parameter is present. | ||||||
```cpp | ||||||
auto next = [i = 0] mutable { return i++; }; | ||||||
next() // 0 | ||||||
next() // 1 | ||||||
next() // 2 | ||||||
``` | ||||||
|
||||||
These may be followed by a `noexcept` specifier, to determine whether calling the lambda may throw an exception. | ||||||
|
||||||
| ||||||
|
||||||
### Template parameters | ||||||
|
||||||
A lambda's `operator()` may be templated to accept template parameters since C++20: | ||||||
|
||||||
```cpp | ||||||
auto lambda = []<typename T>(const T& x) { | ||||||
return x; | ||||||
}; | ||||||
|
||||||
// Deduce template argument from function argument | ||||||
lambda(5); | ||||||
|
||||||
// Pass template argument explicitly | ||||||
lambda.template operator()<double>(5); | ||||||
``` | ||||||
| ||||||
### Attributes | ||||||
Lambdas may be given attributes that apply to their `operator()`s since C++23: | ||||||
```cpp | ||||||
auto very_important_number = [][[nodiscard]] { return 4; }; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very minor style nit but I'd suggest putting a space between the attribute, and the capture block, otherwise it's just a big block of square brackets
Suggested change
|
||||||
``` | ||||||
|
||||||
These attributes are placed right after (optional) template parameters. | ||||||
|
||||||
For details, see [cppreference](https://en.cppreference.com/w/cpp/language/lambda). | ||||||
|
||||||
| ||||||
|
||||||
| ||||||
|
||||||
## Notes | ||||||
|
||||||
### Inheritance | ||||||
|
||||||
Lambdas can be derived from: | ||||||
|
||||||
```cpp | ||||||
auto base = [] { std::puts("Hello, world!"); }; | ||||||
|
||||||
struct : decltype(base) {} derived; | ||||||
|
||||||
derived(); // prints "Hello, world!" | ||||||
``` | ||||||
This, in combination with multiple inheritance, allows for a very neat "visitor" class: | ||||||
```cpp | ||||||
template<typename... Fs> | ||||||
struct visitor : Fs... { | ||||||
using Fs::operator()...; | ||||||
}; | ||||||
visitor { | ||||||
[](int) { std::puts("int"); }, | ||||||
[](double) { std::puts("double"); }, | ||||||
[](...) { std::puts("unknown"); } | ||||||
}(5); // prints "int" | ||||||
``` | ||||||
|
||||||
| ||||||
|
||||||
### Capturing function parameters | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this section is way too niche :) |
||||||
|
||||||
While you can use a function's parameters in its `noexcept` specifier and trailing `requires` clause, using a lambda | ||||||
there which captures the function's parameters is invalid: | ||||||
|
||||||
```cpp | ||||||
// Fine | ||||||
void f(int x) noexcept(noexcept(x)) {} | ||||||
|
||||||
// Invalid | ||||||
void f(int x) noexcept(noexcept([x] { x; })) {} | ||||||
``` | ||||||
This is because the lambda is technically not in function or class scope, and thus cannot have captures. See | ||||||
[expr.prim.lambda.capture#3](https://timsong-cpp.github.io/cppwp/n4950/expr.prim.lambda.capture#3). This can sometimes | ||||||
be worked around using a `requires` expression: | ||||||
```cpp | ||||||
void f(int x) noexcept(requires(decltype(x) x) { | ||||||
requires noexcept([x] { x; }); | ||||||
}) {} | ||||||
``` | ||||||
|
||||||
| ||||||
|
||||||
### Type aliases | ||||||
|
||||||
Making a type alias with an in-line lambda in a header file or module interface violates the | ||||||
[One-Definition Rule](https://en.cppreference.com/w/cpp/language/definition) because aliases are not a "definable item", | ||||||
so lambdas in them are not allowed to match with other lambda declarations in other | ||||||
[translation units](https://en.cppreference.com/w/cpp/language/translation_phases#Translation_process): | ||||||
Comment on lines
+297
to
+300
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I can imagine problems here I'm not seeing why this is an ODR violation, because this isn't a definable https://eel.is/c++draft/basic.def.odr#15 doesn't apply. This is an ODR violation though: https://eel.is/c++draft/basic.def.odr#18. |
||||||
|
||||||
```cpp | ||||||
// Bad | ||||||
using T = decltype([] {}); | ||||||
|
||||||
// Bad | ||||||
template<auto> struct A {}; | ||||||
using T = A<[] {}>; | ||||||
|
||||||
// Ok | ||||||
auto lambda = [] {}; | ||||||
using T = decltype(lambda); | ||||||
``` | ||||||
| ||||||
### In-line partial specialization | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm afraid I don't see why this is useful. It's also not really a specialization, which can be most clearly seen in the index sequence example. I would recommend keeping that example since that is a common useful pattern. |
||||||
Partial specialization usually requires writing out a struct in namespace scope: | ||||||
```cpp | ||||||
template<typename> | ||||||
struct return_type; | ||||||
template<typename Return, typename... Args> | ||||||
struct return_type<Return(Args...)> { | ||||||
using type = Return; | ||||||
}; | ||||||
return_type<int(char)>::type // int | ||||||
``` | ||||||
|
||||||
However, the fact that lambdas can be invoked immediately when they are defined allows doing the same work completely | ||||||
in-line: | ||||||
|
||||||
```cpp | ||||||
decltype( | ||||||
[]<typename Return, typename... Args>(std::type_identity<Return(Args...)>) { | ||||||
return std::type_identity<Return>(); | ||||||
}(std::type_identity<int(char)>()) | ||||||
)::type // int | ||||||
``` | ||||||
|
||||||
Here, `T` and `Return` are wrapped in `std::type_identity` objects to pass them around without actually constructing the | ||||||
types they hold. | ||||||
|
||||||
A similar trick is useful in concepts: | ||||||
|
||||||
```cpp | ||||||
template<typename T, template<typename...> typename Template> | ||||||
concept specialization_of = requires { | ||||||
[]<typename... Args>(std::type_identity<Template<Args...>>) {}(std::type_identity<T>()); | ||||||
}; | ||||||
|
||||||
static_assert(specialization_of<std::tuple<int>, std::tuple>); | ||||||
``` | ||||||
|
||||||
and with `std::integer_sequence`: | ||||||
|
||||||
```cpp | ||||||
[]<std::size_t... i>(std::index_sequence<i...>) { | ||||||
(..., std::print("{} ", i)); | ||||||
}(std::make_index_sequence<5>()); | ||||||
// prints "0 1 2 3 4" | ||||||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.