- [Lambdas](#lambdas)
-- [Before lambdas we had functors](#before-lambdas-we-had-functors)
+- [Overview](#overview)
+- [What is a "callable"](#what-is-a-callable)
+- [A function pointer is sometimes enough](#a-function-pointer-is-sometimes-enough)
+- [Before lambdas we had function objects (or functors)](#before-lambdas-we-had-function-objects-or-functors)
- [How is the sorting function implemented?](#how-is-the-sorting-function-implemented)
- [Enter lambdas](#enter-lambdas)
+- [Lambda syntax](#lambda-syntax)
- [When to use lambdas](#when-to-use-lambdas)
- [Summary](#summary)
-We've talked about so many things, like classes and functions but there is one more thing that modern C++ has that we did not really touch upon - lambdas.
+We've already covered so many topics in this course but there is one more thing that firmly belongs to modern C++ that we did not really touch upon - **lambdas**.
-Here's what they are useful for. Imagine we have a vector of people, represented as a struct `Person`, and we would like to sort them by age. We can use the standard [`std::sort`](https://en.cppreference.com/w/cpp/algorithm/sort) function for that.
+Here's one example for which they are useful. Imagine we have a list of people, represented as a struct `Person`, and we would like to sort them by age. We can try using the standard [`std::sort`](https://en.cppreference.com/w/cpp/algorithm/sort) function for that:
+```cpp
+#include
+#include
+#include
+struct Person {
+ std::string name;
+ int age;
+};
+
+int main() {
+ std::vector people{
+ {"Gendalf", 55'000}, {"Frodo", 33}, {"Legolas", 2'931}, {"Gimli", 139}};
+ // ❌ Won't compile, cannot compare Person objects.
+ std::sort(people.begin(), people.end());
+}
+```
+But the naïve call to `std::sort` will fail and the compiler will throw a loooong error at us:
+
+Long compiler error
+
+```css
+In file included from /opt/compiler-explorer/gcc-10.1.0/include/c++/10.1.0/bits/stl_algobase.h:71,
+ from /opt/compiler-explorer/gcc-10.1.0/include/c++/10.1.0/algorithm:61,
+ from
+
+
+This error message might be quite scary, but if we scroll all the way up, we will see that this error comes down to this line:
+```css
+error: no match for 'operator<' (operand types are 'Person' and 'Person')
+```
+
+Indeed, by default `std::sort` will apply the operator `<` to the provided arguments and, unless we define such operator for our `Person` class, such an operator does not exist.
+
+However, there is an overload of `std::sort` function that we can use! We can provide a lambda expression that compares two `Person` objects.
```cpp
#include
#include
@@ -28,7 +181,8 @@ struct Person {
int age;
};
-void Print(const std::vector& persons) {
+void Print(const std::vector& persons, const std::string& tag) {
+ std::cout << tag << std::endl;
for (const auto& person : persons) {
std::cout << person.name << " " << person.age << "\n";
}
@@ -37,17 +191,28 @@ void Print(const std::vector& persons) {
int main() {
std::vector people{
{"Gendalf", 55'000}, {"Frodo", 33}, {"Legolas", 2'931}, {"Gimli", 139}};
- Print(people);
+ Print(people, "> Before sorting:");
std::sort(
people.begin(), people.end(),
[](const auto& left, const auto& right) { return left.age < right.age; });
- std::cout << "------ sorted --------" << std::endl;
- Print(people);
+ Print(people, "> Sorted by age ascending:");
}
```
-The third argument to the `std::sort` function here is the lambda expression that essentially stands in for a comparison operator between the objects of the person class.
+And now `std::sort` sorts our entries by age in ascending order.
+```
+> Before sorting:
+Gendalf 55000
+Frodo 33
+Legolas 2931
+Gimli 139
+> Sorted by age ascending:
+Frodo 33
+Gimli 139
+Legolas 2931
+Gendalf 55000
+```
-So let's talk about lambdas! What they are, how to write them to stay safe and efficient and, yes, how they make this valid C++ code:
+So let's talk about lambdas! What they are, how to write them in such a way that they operate safely and efficiently and, yes, how they make **this** valid C++ code:
```cpp
int main() {
[](){}();
@@ -56,10 +221,15 @@ int main() {
-## Before lambdas we had functors
-The concept of something "callable" that we can pass into a function or even store for a while is not new to C++. It existed long before lambdas were introduced into the language.
+## Overview
+My aim for today is to walk us through what lambdas are and the reasons they exist. As this topic comes relatively late in our modern C++ course, we have the advantage of being able to understand how lambdas operate using exclusively the things we already know about, mostly functions, classes, and a bit of templates.
-Let's pause for a moment and talk a bit about what it means that something is "callable". Essentially it means that we can call it through a `()` operator with the expected number of arguments. So if we write a function `less(const Person&, const Person&)` and pass its pointer to `std::sort` it will do the trick:
+## What is a "callable"
+As a first step, though, I'd like to briefly talk about what it really means that something is "callable". Clearly, function is "callable" because we can, well, call it. By extension, we can claim that anything that we can call through an `operator()` with the expected number of arguments is also a "callable".
+
+
+## A function pointer is sometimes enough
+As we've just mentioned, the simplest "callable" is a function. In our example from before, if we write a function `less(const Person&, const Person&)` and pass its pointer to `std::sort` it will do the trick:
```cpp
#include
#include
@@ -71,7 +241,8 @@ struct Person {
int age;
};
-void Print(const std::vector& persons) {
+void Print(const std::vector& persons, const std::string& tag) {
+ std::cout << tag << std::endl;
for (const auto& person : persons) {
std::cout << person.name << " " << person.age << "\n";
}
@@ -82,10 +253,10 @@ bool less(const Person& p1, const Person& p2) { return p1.age < p2.age; }
int main() {
std::vector people{
{"Gendalf", 55'000}, {"Frodo", 33}, {"Legolas", 2'931}, {"Gimli", 139}};
- Print(people);
+ Print(people, "> Before sorting:");
+ // 💡 We can also pass "less" without "&" here. Try it!
std::sort(people.begin(), people.end(), &less);
- std::cout << "------ sorted --------" << std::endl;
- Print(people);
+ Print(people, "> Sorted by age ascending:");
}
```
Note that we can also drop the `&` such that the call to `std::sort` becomes:
@@ -93,12 +264,14 @@ Note that we can also drop the `&` such that the call to `std::sort` becomes:
std::sort(people.begin(), people.end(), less);
```
The reason for this is that [functions are implicitly converted to function pointers](https://en.cppreference.com/w/cpp/language/implicit_conversion#Function-to-pointer_conversion) if needed, they are special in this way.
+
-But what if this is not enough? What if we need to have a certain state? For example, we wouldn't want to sort the people by their absolute age, but by the difference of their age with respect to some number, say `4242`.
+## Before lambdas we had function objects (or functors)
+But what if this is not enough for our use-case? What if we need to have a certain state stored in our "callable"? For example, we wouldn't want to sort our `Person` objects by their absolute age, but by the difference of their age with respect to some number, say `4242`.
-Behold **function objects**, or **functors**. These are objects for which the function call operator is defined, or, in other words, that define an operator `()`.
+Behold **function objects**, or **functors**. These are objects for which the function call operator is defined, or, in other words, that define an `operator()`.
-So, if we want to sort our array by the age difference to `4242` we can create a struct `ComparisonToQueryAge` that has a member `query_age_` and an operator `()` that compares the age differences instead of directly the ages:
+So, if we want to sort our array by the age difference to some number, we can create a struct `ComparisonToQueryAge` that has a member `query_age_` and an `operator(const Person&, const Person&)` that compares the age differences of the two provided `Person` objects instead of directly their ages:
```cpp
#include
#include
@@ -110,16 +283,17 @@ struct Person {
int age;
};
-void Print(const std::vector& persons) {
+void Print(const std::vector& persons, const std::string& tag) {
+ std::cout << tag << std::endl;
for (const auto& person : persons) {
std::cout << person.name << " " << person.age << "\n";
}
}
struct ComparisonToQueryAge {
- ComparisonToQueryAge(int query_age) : query_age_{query_age} {}
+ explicit ComparisonToQueryAge(int query_age) : query_age_{query_age} {}
- bool operator()(const Person& p1, const Person& p2) const noexcept {
+ bool operator()(const Person& p1, const Person& p2) const {
return std::abs(p1.age - query_age_) < std::abs(p2.age - query_age_);
}
@@ -129,48 +303,22 @@ struct ComparisonToQueryAge {
int main() {
std::vector people{
{"Gendalf", 55'000}, {"Frodo", 33}, {"Legolas", 2'931}, {"Gimli", 139}};
- Print(people);
+ Print(people, "> Before sorting:");
std::sort(people.begin(), people.end(), ComparisonToQueryAge{4242});
- std::cout << "------ sorted --------" << std::endl;
- Print(people);
+ Print(people, "> Sorted by age difference to 4242, ascending:");
}
```
+Once we pass this struct as the callable into the `std::sort`, we can see that our Tolkien characters are sorted by their age difference to the number `4242`.
## How is the sorting function implemented?
-So far so good. We already know a lot about structs and classes as well as their methods, so I hope that how these operate seems quite intuitive here. Furthermore, thinking back to the lectures in which we covered templates we can also imagine how to implement a function similar to `std::sort` that would take any object that is "callable" by using templates:
-```cpp
-template
-void MySort(Iterator begin, Iterator end, Comparator comparator) {
- // Sort using comparator(*iter_1, *iter_2) as a building block.
-}
+So far so good. We already know a lot about structs and classes as well as their methods, so I hope that how these operate seems quite intuitive here.
-int main() {
- std::vector people{
- {"Gendalf", 55'000}, {"Frodo", 33}, {"Legolas", 2'931}, {"Gimli", 139}};
- MySort(people.begin(), people.end(), ComparisonToQueryAge{4242});
- MySort(people.begin(), people.end(), less);
- MySort(people.begin(), people.end(), &less);
-}
-```
+Now I think it makes sense to look a bit deeper into how `std::sort` is implemented. How does it magically take anything that looks like a "callable"?
-And the story doesn't end with `std::sort`. There is a number of functions that take these function objects. For some example, see `std::find_if`, `std::for_each`, `std::transform`, etc.
+Please pause here for a moment and think how would you implement this! I promise you that if you followed the previous lectures, you should have all the tools at your disposal by now.
-## Enter lambdas
-However, it might not be convenient to always define a new struct, class, or even function for any use case. Sometimes we want to use such a function object only locally, within a function and don't want any overhead.
-
-That convenience is what brought us the lambdas. This is really just a syntactic sugar for defining out own function objects using special syntax.
-
-The syntax of defining a lambda is the following:
+The key is to think back to the lectures in which we covered templates! We can hopefully all imagine that using templates would allow us to implement a function similar to `std::sort`. Our interest here is _not_ to implement a better sorting algorithm, but to gain intuition about how we _could_ implement such a generic algorithm that would take any comparator object that is "callable" and accepts two `Person` objects as its parameters:
```cpp
-[const] auto LambdaName = [CAPTURE_LIST](ARGUMENTS){BODY} -> ReturnType;
-// We can call it with
-LambdaName(ARGUMENTS);
-```
-So now you see that `[](){}()` is just a definition of a lambda that has an empty capture list, no arguments, empty body, which is called in-place right after creation (doing nothing of course). Totally useless, but a completely valid syntax!
-
-We can replace all of our use-cases with such lambdas:
-```cpp
-#include
#include
#include
#include
@@ -180,33 +328,62 @@ struct Person {
int age;
};
-void Print(const std::vector& persons) {
+void Print(const std::vector& persons, const std::string& tag) {
+ std::cout << tag << std::endl;
for (const auto& person : persons) {
std::cout << person.name << " " << person.age << "\n";
}
}
+struct ComparisonToQueryAge {
+ explicit ComparisonToQueryAge(int query_age) : query_age_{query_age} {}
+
+ bool operator()(const Person& p1, const Person& p2) const {
+ return std::abs(p1.age - query_age_) < std::abs(p2.age - query_age_);
+ }
+
+ int query_age_{};
+};
+
+bool less(const Person& p1, const Person& p2) { return p1.age < p2.age; }
+
+template
+void MySort(Iterator begin, Iterator end, Comparator comparator) {
+ // The actual algorithm is not important here.
+ for (Iterator i = begin + 1; i != end; ++i) {
+ Iterator j = i;
+ // We call comparator(*iter_1, *iter_2) somewhere in our algorithm.
+ while (j != begin && comparator(*j, *(j - 1))) {
+ std::iter_swap(j, j - 1);
+ --j;
+ }
+ }
+}
+
int main() {
std::vector people{
{"Gendalf", 55'000}, {"Frodo", 33}, {"Legolas", 2'931}, {"Gimli", 139}};
- Print(people);
- std::sort(people.begin(), people.end(),
- [](const auto& p1, const auto& p2) { return p1.age < p2.age; });
- std::cout << "------ sorted --------" << std::endl;
- Print(people);
-
- int query_age = 4242;
- std::sort(people.begin(), people.end(),
- [query_age](const auto& p1, const auto& p2) {
- return std::abs(p1.age - query_age) <
- std::abs(p2.age - query_age);
- });
- std::cout << "------ sorted --------" << std::endl;
- Print(people);
+ Print(people, "> Before sorting:");
+ MySort(people.begin(), people.end(), less);
+ Print(people, "> Sorted by age ascending:");
+ MySort(people.begin(), people.end(), ComparisonToQueryAge{4242});
+ Print(people, "> Sorted by age difference to 4242, ascending:");
+ MySort(people.begin(), people.end(), &less);
+ Print(people, "> Sorted by age ascending:");
}
```
+Note also that from C++20 on this code would become more readable as we could use concepts instead of templates.
+
+
+Oh, one more thing, the story of course doesn't end with `std::sort`! There is a number of functions that take similar function objects. For some example, see [`std::find_if`](https://en.cppreference.com/w/cpp/algorithm/find#Version_3), [`std::for_each`](https://en.cppreference.com/w/cpp/algorithm/for_each), [`std::transform`](https://en.cppreference.com/w/cpp/algorithm/transform), and many more.
+
+## Enter lambdas
+However, it might not be convenient to always define a new struct, class, or even function for every single use case. Sometimes we want to use such a function object only locally, once, and don't want to create any additional overhead.
-Furthermore we can store a lambda in a variable and reuse it multiple times. In our example, we can observe that we use `Print` function only in our main function here. While there is no issue with this function being a standalone function in an unnamed namespace, we might as well make it a lambda:
+The strive to enable such convenience is what brought us the [lambda expressions](https://en.cppreference.com/w/cpp/language/lambda), or, colloquially, **lambdas**. They are really just syntactic sugar for defining our own function objects as discussed before.
+
+## Lambda syntax
+The syntax of defining a lambda expression is a little different from what we've seen before. Let's modify our example to use lambdas instead of functions and function objects and look closer at how we can define and use lambdas in our programs.
```cpp
#include
#include
@@ -219,7 +396,8 @@ struct Person {
};
int main() {
- const auto Print = [](const auto& persons) {
+ const auto Print = [](const auto& persons, const auto& tag) {
+ std::cout << tag << std::endl;
for (const auto& person : persons) {
std::cout << person.name << " " << person.age << "\n";
}
@@ -227,25 +405,109 @@ int main() {
std::vector people{
{"Gendalf", 55'000}, {"Frodo", 33}, {"Legolas", 2'931}, {"Gimli", 139}};
- Print(people);
+ Print(people, "> Before sorting:");
+
std::sort(people.begin(), people.end(),
[](const auto& p1, const auto& p2) { return p1.age < p2.age; });
- std::cout << "------ sorted --------" << std::endl;
- Print(people);
+ Print(people, "> Sorted by age ascending:");
- int query_age = 4242;
+ const int query_age = 4242;
std::sort(people.begin(), people.end(),
- [query_age](const auto& p1, const auto& p2) {
+ [query_age](const auto& p1, const auto& p2) -> bool {
return std::abs(p1.age - query_age) <
std::abs(p2.age - query_age);
});
- std::cout << "------ sorted --------" << std::endl;
- Print(people);
+ Print(people, "> Sorted by age difference to 4242, ascending:");
+}
+```
+
+Here, we use 3 different lambdas. All of them follow the same general syntax that largely looks like this:
+```cpp
+auto LambdaName = [CAPTURE_LIST](ARGUMENTS){BODY} -> ReturnType;
+```
+They all have some **arguments** (that can be omitted should they not be needed), a **body** that defines what the lambdas actually do, a return type, that we can also provide explicitly but if we don't, it will be deduced from the `return` statement within the lambda function.
+
+If we assign our lambda to a variable, we can store our lambda object and reuse it multiple times. We do this in our example for the `Print` lambda. If you're wondering, the type of this lambda will be some unique unnamed type that the compiler will make up on its own.
+
+Now it is time we talk about the **capture list**. It is a new thing to us and is the syntax that we can easily recognize using a lambda by.
+
+The first two lambdas we use have an empty capture list, but one captures the `query_age` variable in it:
+```cpp
+[query_age](const auto& p1, const auto& p2) -> bool {
+ return std::abs(p1.age - query_age) < std::abs(p2.age - query_age);
}
```
+What this really means is that the `query_age` variable is copied such that it becomes available inside of the lambda body. If we look back at function objects we discussed before, this lambda behaves exactly the same as `ComparisonToQueryAge` struct from before.
+
+In our case, `query_age` is a small variable - a single `int`. But if we wanted to capture a bigger variable, just like with function arguments, we would like to avoid unnecessary copies, so we'd like to capture it by reference. Any variable we would like to capture by reference we prefix by an ampersand `&` symbol. Just an an illustration, this would capture `query_age` by reference instead of copying it:
+```cpp
+[&query_age](const auto& p1, const auto& p2) -> bool {
+ return std::abs(p1.age - query_age) < std::abs(p2.age - query_age);
+}
+```
+
+We can provide as many captured variables as we want, specifying for each if we want to capture them by copy or by reference.
+```cpp
+[&one, two, &three] {
+ // Do something with one, two, three.
+}
+```
+Here, `one` and `three` will be captured by reference, while `two` is captured by copy.
+
+
+Alternatively, we can capture all variables visible at the moment of lambda definition. There are three distinct cases that are worth talking about here.
+
+If we want to capture all variables by copy, we can use `=` as the first capture. And if we want some variables to be captured by reference we can specify such variables further in the capture list.
+```cpp
+int one{};
+float two{};
+double three{};
+[=] {
+ // All variables are captured by copy.
+}
+[=, &two] {
+ // two is captured by reference, all the others by copy.
+}
+```
+
+Should we want to capture all variables by reference instead, we can pass a single ampersand `&` symbol instead. Should we want _some_ variables to still be captured by copy, we can simply append them to the capture list.
+```cpp
+int one{};
+float two{};
+double three{};
+[&] {
+ // All variables are captured by reference.
+}
+[&, two] {
+ // two is captured by copy, all the others by reference.
+}
+```
+
+Finally, if a lambda appears within a class method, we might want it to have access to all the data within the current object. For that we can pass `this` into the lambda capture list and use the object data without issues:
+```cpp
+struct Foo {
+ void Bar() {
+ [this] {
+ // Current object is captured by reference.
+ }
+ }
+
+ int one{};
+ float two{};
+ double three{};
+};
+```
+
+And as a small bonus, now that we've discussed most of the syntax we use for lambdas, we can see that `[](){}()` from the thumbnail of the video is just a definition of a lambda that has an empty capture list, no arguments, empty body, which is called in-place right after creation (doing nothing of course). This lambda is totally useless, apart from the entertainment it provides :wink:
+
## When to use lambdas
-Lambdas are neat and efficient. If you need an operation that you don't think you'll need to reuse to pass into some other function, like in our example with sorting, lambdas are your friend. Alternatively, if you are implementing some functionality in a header file and find yourself writing a bit of a longer function, lambdas are usually a better way to split such function into meaningful chunks without introducing public-facing functions and not relying on comments that can easily go out of sync with the code functionality.
+Lambdas are neat and efficient. If you need an operation that you don't think you'll need to reuse to pass into some other function, like in our example with sorting, lambdas are your friend. Alternatively, if you are implementing some functionality in a header file and find yourself writing a bit of a longer function, lambdas are a useful way to split such function into meaningful chunks without introducing public-facing functions and not relying on comments that can easily go out of sync with the code functionality. So use them without fear in most situations.
+
+One thing to be weary of is capturing all variables by default. While it might be tempting to always capture all the observed variables by reference by providing the ampersand in the capture list `[&]`, in my experience it makes it harder to keep track of what the lambda really does. So in most of my code, I prefer to capture only the variables I _really_ need as opposed to blanket-capturing them.
+
## Summary
-TODO
+Now we should know most of the things we need to know about what lambdas are as well as about how and when we should use them. They are a useful tool in our toolbox and we'll find that we want to use them quite often when writing modern C++ code. I hope that I could build parallels with what we have already learnt until now so that you can get all the use our lambdas while not being scared of what they do under the hood.
+
+