From 21e59c18ec513b1f16e8ec4df76c41c72b043acb Mon Sep 17 00:00:00 2001 From: Igor Bogoslavskyi Date: Tue, 15 Oct 2024 21:45:40 +0200 Subject: [PATCH] Final touches --- lectures/lambdas.md | 54 ++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/lectures/lambdas.md b/lectures/lambdas.md index 621483c..6d64814 100644 --- a/lectures/lambdas.md +++ b/lectures/lambdas.md @@ -165,14 +165,14 @@ Compiler returned: 1
-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: +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 that states that there is no "less than" operator defined for the two `Person` objects: ```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, this operator does not exist. +You see, by default `std::sort` will apply the operator `<` to the provided arguments and, unless we define such operator for our `Person` class, this 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. +However, there is another overload of `std::sort` function that we can use! We can provide a **lambda expression** that compares two `Person` objects. ```cpp #include #include @@ -201,7 +201,7 @@ int main() { Print(people, "> Sorted by age ascending:"); } ``` -And now `std::sort` sorts our entries by age in ascending order. +And now `std::sort` sorts our Tolkien characters by age in ascending order. ``` > Before sorting: Gendalf 55000 @@ -226,12 +226,12 @@ int main() { ## Overview -My aim for today is to walk us through what lambdas are and the reasons they exist as well as roughly how they work under the hood. As this topic comes relatively late in our modern C++ course, we have the advantage of being able to understand how lambdas operate using a bunch of things we already know about, like functions, classes, and a bit of templates. +My aim for today is to walk us through what lambdas are and the reasons they exist as well as roughly talk about how they work under the hood. As this topic comes relatively late in our modern C++ course, we have the advantage of being able to understand how lambdas operate using a bunch of things we already know about, like functions, classes, and a bit of templates. ## What is a "callable" As a first step, though, I'd like to briefly talk about what `std::sort` does to whatever third argument we pass into it. It, well, calls it with two `Person` objects as the input arguments. But what do we really mean, when we say that something gets "called"? -Clearly, we can "call" a function, more or less by definition. By extension, we can claim that anything that we can call through an `operator()` with the expected number of arguments is also "callable". Which opens a whole new perspective on how to create these "callable" things. +Clearly, we can "call" a function, more or less by definition. By extension, we can claim that anything that we can call through an `operator()` is also "callable". Which opens a whole new perspective on how to create these "callable" things. ## A function pointer is sometimes enough In most cases, simple is good enough. As we've just mentioned, the simplest "callable" is a function. In our example from before, we don't _really_ need to use a lambda. If we write a function `less` that takes two `Person` objects and pass its pointer to `std::sort` it will do the trick and the objects will get sorted: @@ -281,15 +281,15 @@ int main() { ```cpp std::sort(people.begin(), people.end(), less); ``` -The reason for this is that [functions can be implicitly converted to function pointers](https://en.cppreference.com/w/cpp/language/implicit_conversion#Function-to-pointer_conversion), they are special in this way. +The reason for this, if you are interested, is that [functions can be implicitly converted to function pointers](https://en.cppreference.com/w/cpp/language/implicit_conversion#Function-to-pointer_conversion), they are special in this way. ## Before lambdas we had function objects (or functors) -But 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`. +But what if we need to have a certain *state* stored in our "callable"? For example, what if 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**](https://en.cppreference.com/w/cpp/utility/functional), or **functors**. These are objects for which the function call operator is defined, or, in other words, that define an `operator()`. So they are also "callable". +Behold [**function objects**](https://en.cppreference.com/w/cpp/utility/functional), or sometimes also **functors**, although we should prefer the former name to avoid confusion. These are objects for which the function call operator is defined, or, in other words, that define an `operator()`. As they define this operator, they can be "called" and so they are also "callable". -Which means that if we want to sort our array by the age difference to some number, we can create a class `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: +Which means that if we want to sort our array by an age difference to some number, we can create a class `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 comparing their ages between each other: ```cpp #include #include @@ -328,16 +328,18 @@ int main() { Print(people, "> Sorted by age difference to 4242, ascending:"); } ``` -Once we pass an object of this class as the callable into the `std::sort`, we can see that our Tolkien characters are sorted by their age difference to the number `4242`. +Once we pass an object of this class as the callable into the `std::sort`, we can see that our Tolkien characters are now sorted by their age difference to the number `4242`. ## How to implement generic algorithms like `std::sort` So far so good. We already know a lot about classes so I hope that what we've just covered seems quite self-explanatory. 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 just rolls with it? -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. +Please pause here for a moment and think how would **you** implement this! I promise you that if you followed the previous lectures, especially those on templates, you should have all the tools at your disposal by now. -The key is to think back to the lectures in which we covered [templates](templates_why.md)! We can hopefully all imagine that using templates would allow us to implement a function similar to `std::sort`: + + +The key is to think back to the lectures in which we covered [templates](templates_why.md)! We can hopefully all imagine that using templates would allow us to implement a function similar to `std::sort`, where the begin and end iterators as well as the comparator callable all have some template type that is guessed by the compiler at compile time: ```cpp #include #include @@ -394,15 +396,15 @@ int main() { Print(people, "> Sorted by age ascending:"); } ``` -Note though that our interest here is _not_ to implement a better sorting algorithm (so feel free to ignore the actual implementation) but to gain intuition about how we _could_ implement a generic algorithm that takes any comparator object that is "callable" with two `Person` objects. We don't have to write much code to achieve this too! And if we run this, we get the expected output. +Note though that our interest here is _not_ to implement a better sorting algorithm (so feel free to ignore the actual implementation) but to gain intuition about how we _could_ implement a generic algorithm that takes any comparator object that is "callable" with two `Person` objects, be it a function pointer or a function object. We don't have to write much code to achieve this too! And if we run this, we get the expected output. Note also that from C++20 on this code would become more readable and safe as we could use concepts instead of raw 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. +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 examples, 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 deal with any additional boilerplate code. +However, it might not be convenient to always define a new struct, class, or even a function for every single use case. Sometimes we want to use such a function object only locally, once, and don't want to expose it to the outside world, nor to deal with any additional boilerplate code. 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, just like the `ComparisonToQueryAge` class we talked about before. @@ -456,7 +458,7 @@ They all have some **arguments** (that can be omitted should they not be needed) If we assign our lambda to a variable, we can store our lambda object and reuse it multiple times as we do for the `Print` lambda. And if you were 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 lambdas by. +Now it is time we talk about the stuff inside the square brackets in each lambda definition - the **capture list**. It is a new thing to us and is the syntax that we can easily recognize lambdas by. The first two lambdas we use have an empty capture list, but the third one captures the `query_age` variable in it: ## Summary @@ -708,4 +709,7 @@ And this is pretty much most of the things we need to know about lambdas. But if All in all, lambdas 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 out of lambdas while not being scared of what they do under the hood. - +