Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ export default defineConfig({
collapsed: true,
},
{ text: "Standards", link: "/resources/general/standards" },
{
text: "C++ Resources",
items: [{ text: "Lambdas", link: "/resources/general/cpp/lambdas" }],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
items: [{ text: "Lambdas", link: "/resources/general/cpp/lambdas" }],
items: [{ text: "Lambdas", link: "/resources/cpp/lambdas" }],

collapsed: true
},
{ text: "Project Ideas", link: "/resources/general/project-ideas" },
{
text: "Advanced Resources",
Expand Down
365 changes: 365 additions & 0 deletions src/resources/cpp/lambdas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
## Abstract
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every page should start with an h1, that's how pages get page titles. E.g.

image

vs

image

I would also say let's not make an "abstract" section, this isn't a paper :)


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:
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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; });
}
```

&nbsp;
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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.


&nbsp;

## Syntax

The basic syntax of a lambda expression looks like this:

```
[captures](params) -> ReturnType {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[captures](params) -> ReturnType {
[captures](params) -> return_type {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-_-

statements;
}
```
Comment on lines +46 to +50
Copy link
Member

Choose a reason for hiding this comment

The 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).

&nbsp;

Each lambda expression has its own unique, unnameable type:
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Author

Choose a reason for hiding this comment

The 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 operator() is useful early on, so should I write something else here instead?

Copy link
Member

Choose a reason for hiding this comment

The 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)>);
```
&nbsp;
### 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
Copy link
Member

Choose a reason for hiding this comment

The 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!"

Copy link
Author

Choose a reason for hiding this comment

The 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;
}();
}();
```

Copy link
Member

Choose a reason for hiding this comment

The 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 [&] and [=] can be used without fear of bloat or overhead

&nbsp;

### 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:
Copy link
Member

Choose a reason for hiding this comment

The 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);
};
```

&nbsp;

### 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`.
&nbsp;
### 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.
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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 static:

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.

&nbsp;

### 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);
```
&nbsp;
### Attributes
Lambdas may be given attributes that apply to their `operator()`s since C++23:
```cpp
auto very_important_number = [][[nodiscard]] { return 4; };
Copy link
Member

Choose a reason for hiding this comment

The 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
auto very_important_number = [][[nodiscard]] { return 4; };
auto very_important_number = [] [[nodiscard]] { return 4; };

```

These attributes are placed right after (optional) template parameters.

For details, see [cppreference](https://en.cppreference.com/w/cpp/language/lambda).

&nbsp;

&nbsp;

## 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"
```

&nbsp;

### Capturing function parameters
Copy link
Member

Choose a reason for hiding this comment

The 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; });
}) {}
```

&nbsp;

### 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
Copy link
Member

Choose a reason for hiding this comment

The 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);
```
&nbsp;
### In-line partial specialization
Copy link
Member

Choose a reason for hiding this comment

The 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"
```
Loading