Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
So this is part of the Back to Basics track. I hope you all enjoying the Back to Basics track; and I hope that you are rating the talks through the feedback site. If you're not, please do! Please tell us if they're good, if they're bad, what we can do to improve. And we'll get better next year. All right, so, welcome to "Lambdas from Scratch." I'm Arthur O'Dwyer. Raise your hand if you've seen this talk before — a talk with this title. Or "Generic Lambdas from First Principles." I don't see any hands. I don't see any people. [SHIELDING EYES FROM SPOTLIGHT] It's terrible. All right. But, if you are raising your hand, then please know that this will be very similar material to what you saw in those previous talks. This talk has been presented here before. I've updated a bit for what's coming— what is here now, in C++17, and what's coming in C++20. But the bulk of it, the outline, is going to be the same. So originally I called this talk "Lambdas from First Principles." Before I got on to my whole "...From Scratch" kick. And the reason that I called it "from First Principles" is that we're going to start at the beginning. We're going to start with C, and with functions. C++ also has functions but it got them from C. The structure of a C++ function looks just like a C function. It's very old technology. We have a function here. Its name is `plus1`. It takes an int and returns an int. And its behavior is that it adds 1— it returns `x+1`. The compiler can take this and compile it down into some machine code. I'm going to be showing machine code (or, I should say, assembly code) alongside here. And you can follow along in Godbolt Compiler Explorer if you want. Type in the code; see what that produces on the right-hand side. So, functions are easy. Now C++ added some more things in that general function space. The first thing that C++ added was function overloading — the ability to have two different functions both named `plus1`. But to, in a sense, have a little bit of polymorphism, right? Static polymorphism, but polymorphism nonetheless. Because I can now say `plus1(y)` and I don't have to care, as the human programmer, what type `y` is. If it's an int, then the compiler will call `plus1(int)`. If it's a double, the compiler will call `plus1(double)`. And of course there's overload resolution that helps us figure out which overload to call. So we have function overloading. But we have two functions now, and they are both named `plus1`. How does the linker know that these are two distinct functions? Well, the way that C++ decided to do it is to mangle the names of the arguments into the linker symbol that represents the function. So over on the right-hand side here, we see the code generation again. And we see we have two `plus1` functions, but in one of them the mangled name contains an `i`, for int; and in the other case it contains a `d`, for double. And they have very different code inside themselves. Right? Because adding 1 to an `int` is a different operation, at the machine level, than adding 1 to a `double`... ...even though in the source code they look the same. So C++ gives us this tool. However, in this case we still had to write out the body twice, and the body is the same in both cases — `return x+1`. I have to write a different version of `plus1`, a different overload, for `int`, for `short`, for `double`... ...for any other type that I want. There are a lot of types. I would like to automate this away I would like to have a template for stamping out functions of that form — when the body is always the same, or just parametrized a little bit. In this case the only parameter, the only thing I'm changing about these various overloads, is what type they take and what type they return — and that's always the same type. I'll just call that type `T`. And I'll make a template. A function template. Now, a function template is not a function. It's a template for stamping out functions. You put in a `T`, you get out a function. So in that case, notice again that the compiler is doing something a little bit different on the right-hand side. The template instantiations it's producing don't have mangled names that look quite the same... ...as if it were a simple function that was overloaded with different signatures... That is, multiple functions, all in the same overload set. When I have a function template, the mangled names look a little bit different again. But we see that we have the `i` and the `d` showing up again in the mangled names. So now when I say `auto y = plus1(42);` the compiler uses template type deduction, and it figures out that 42 is an `int`. Which `plus1` should I call? Well, I have a template for stamping out functions. So I can put in whatever `T` I want. What should T be? It does template type deduction. It deduces that `T` should be int`... ...and it instantiates `plus1<int>`. For `plus1(3.14)` it'll instantiate `plus1<double>`, and it will stamp out the code on the right. C++ also gave us the ability to put functions inside classes. They're called member functions. Right? I can have a class member function... Here I have a class capital-P `Plus`, and it has a data member named `value`, of type `int`. It has a constructor, which we'll say takes this `int v` and initializes `value` with `v`. "Not shown." And then we have a member function, `plusme`. it takes an int and returns an int. It also takes that hidden `this` parameter, so that it can access the data members of the class capital-P `Plus`. And it's a `const` member function. I don't have to modify the `Plus` object itself. I don't have to modify `*this` inside this function. I'm just reading it. So I make it a const member function. And this generates yet again a little bit different code So C++ gave us class member functions. Now, C++ also— Oops, nope, we're not there yet. Sorry. Which function do we call? When I have an object of type `Plus`... So, `Plus(1)`... and I put that in a variable, a named variable, named `plus`, and I say `plus.plusme(42)`... how does the computer know that I intend to call the `plusme` method of the `Plus` class in particular? How does it know... ...that I want to call THAT `plusme` function and not some OTHER `plusme` function? Well, that's static typing. C++ is not Java! In Java it would look something like this. I have a variable in my function's stack frame. And it is a reference to an object that lives out on the heap. Right? Java has object types. Many languages work this way: JavaScript. Python. Out there on the heap I have this object. And it has a table of behaviors. The object knows about its various behaviors. So it has what we call a vptr to a vtable. And the vtable has function pointers to these various behaviors. And if you want to know, "what does the `plus` object do when you say `plusme` on it" — you ask the `plus` object! You follow the pointer to the object... Oops, let's use my mouse pointer. You follow the pointer to the object. You follow the vptr to the table of behaviors. You follow the function pointer to the actual behavior. C++ lets you do this, but this is not the default. This is very inefficient. We don't want to do this. The C++ approach is, we take the object and we put it right there on the stack — in the stack frame. When I declare an object whose type is `Plus`, I get an actual `Plus` object right there on the stack, just the same as if I had declared an `int`. C++ treats these types the same as the fundamental types. When I declare a `std::string`, I get a `std::string` right there on the stack. It may manage a heap allocation as part of what it does, but the object itself— I have `sizeof(that-object-type)` bytes right there on the stack. And there are no pointers. We got rid of all the pointers. Not just the pointer to the heap allocation! We got rid of the vptr. There's no vptr. There's no vtable, no table of behaviors in C++. Unless you explicitly ask for one by making one or more of your methods virtual. Instead, the compiler KNOWS that when you call the `plusme` method on an object whose static type is `Plus`... ...you must mean the `plusme` method that is a member of `Plus`. So I've indicated that with just a little dotted line here. "Static typing for the win." A little dotted line. And there are no pointer dereferences showing up in the generated code. The generated code simply calls the constructor of `Plus`, and then it calls the `plusme` method of `Plus`. The compiler knows the names of those functions, and it knows what they do. And it can inline them, even. I had to turn off inlining to get it to generate this code. So class member functions look like this. C++ also gives us something else. The other thing C++ gives us is operator overloading. I can overload the `+` operator. I can overload the `*` operator. I can overload... ...the function call operator! The function call operator is spelled `operator()`. And when I do that, I no longer call that member function by saying `plus.plusme(...)` and passing arguments. I just say `plus(...)` and pass some arguments. This is what happens when you have an overloaded `operator()`. So I can call it as we see down in the lower right-hand corner here. I make my `Plus` object, constructed with the integer 1. I assign that to a variable `plus`. And then I use that variable `plus` and I call it like a function. That calls the overloaded call operator, and the call operator can do whatever I want. And it's still marked `const`. Because I still don't need to modify the object in order to call its call operator. So now we can make something kind of nifty. You may have noticed: on this slide, there's a lot of boilerplate. Everything in green is basically boilerplate. It's not relevant to the task I'm trying to accomplish. Everything in bold and red — that is the irreducible complexity of this code. If I want to accomplish this task that I've set out to accomplish, I'm going to need something... that holds within itself `int value`... that is, a data member named `value`, of type `int`. The compiler isn't going to figure that out for me. I had to type that out. It has something that takes an int and returns an int. Returns `x + value`. But everything else — everything in green — is boilerplate, and we can just get rid of that. We're going to get rid of all of that green text and we're going to replace it with... Uh, braces. Braces and brackets and parentheses. One of each. That's right. So, this is a lambda. This means the exact same thing as what we had on the previous slide. And it's used exactly the same way. On the previous slide I had a class `Plus`, and it had a data member named `value` that was initialized in the constructor... and it had an `operator()` that did this and was `const`. On the next slide, I still have a variable... but it's no longer a `Plus` variable. Because that name was part of my reducible complexity. I got rid of it. I don't mean to remember that name. That name is not important. The lambda is still a variable of class type — or, an object of class type, that I'm assigning to a variable. And that `plus` object is still CONCEPTUALLY an object of type `Plus`. but its name has gone away. The compiler is going to pick a name for it. And it's going to pick something that I can't spell, other than through taking the `decltype` of that object. It has the exact same implementation. `plus` is still an object. It's not a `Plus` object. We're going to say it's a `$_0` object, because that's what Clang does. And it has that same sort of static relationship with the code for its call operator. And the codegen looks exactly the same. So we haven't lost anything at all by getting rid of that name, that capital-P `Plus`. So this gives us the ability to have Lisp-style, Python-style closures... ...without introducing heap-allocation, garbage collection, runtime polymorphism... We didn't do any of that, right? We just took a very simple class `Plus`, and we got rid of its name, and we gave you some syntactic sugar. That's all a lambda is. So how does this interact with capturing things? Well. Here I have a function `contains_title`. It takes a reference to a shelf full of books, and it takes a title of one of those books as a `std::string`. (I could have passed it by reference. I decided, on this slide, I'm not going to.) And I make a lambda. My lambda has one data member named `t`. Lowercase `t`. That data member is initialized when I construct my lambda, which is happening right here. I initialize it with a copy of `title`. It has an `operator()` that takes a `const Book& b`, and asks that book for its title, and compares that to the captured data member `t`. And then I can pass that lambda off to `std::find_if`. Right? I can pass that lambda around just like a regular object — just like an object of class type. Because that's what it is. It's an object of class type. I just can't give the name of its type, exactly. So how that would look is, here I have my parameter `title`. It lives on the stack. It maybe manages some heap allocation, out there on the heap, right?— the contents of the string. And when I make my lambda, and I make a copy of `title` into that data member — the string data member named `t` of that object... That's just calling the copy constructor. So here's my `has_title_t` object. It's of type `$_1`. I can't name what type it is. But the compiler gave it a type. I can't SPELL what type it is, I should say. And that lambda has a data member of type `std::string`, named `t`, initialized with a copy of the string. You can think about this like— when you're looking at the capture list, think about putting an `auto` on the front. You don't actually put the `auto` in the front, But that's essentially a good enough way to think about what the syntax is doing. If I say `auto t = title;` that makes a copy of the string. That calls the copy constructor. So likewise when I have, in square brackets, for a lambda, `[t = title]`, the lambda is capturing a copy of the string. It's calling the copy constructor What if I didn't want to make that copy? Well, I could capture a pointer. How would I capture a pointer? Well, if I said `auto pt = &title;` that would give me a variable of type `std::string*` — a pointer to a `std::string`. So I can just remove the `auto` and put brackets around it. And now I've captured a pointer to a string. But that looks a little different. You know, I have to dereference the pointer in here. So, we can do a little bit better. We also have added syntactic sugar for lambdas in C++11... ...so that I can do this. When I say `auto &t = title;` that means `t` is a reference to `title`. I didn't make a copy. `t` is a reference. So likewise I can drop the `auto`, and put the square brackets in my lambda. When I have a lambda, where in the square brackets I say `[&t = title]`, that means `t` is a reference... ...to the `title` from the outer scope. To that `title` variable. And so it would look something like this. Notice that my lambda is getting smaller. I'm not copying the string object anymore. That 24 or 32 bytes of string. I'm just holding an 8-byte reference to the title. But when we capture by reference, we do have to be a little bit careful. Because that reference might become dangling. If I capture a reference to a local variable, and then I return that lambda... Then the returned lambda will have within itself a reference that refers to the `title` object — the local variable or the parameter of my function, which has now returned — that object has been destroyed — I have a dangling reference. So if you're going to be making lambdas that you're going to be passing up the call stack — or across, into another thread — Not passing down into a standard algorithm like `std::sort`. That's fine. Capture by reference in that case. But if you're passing them up — returning them — or passing them across to a different thread, then you want to be careful about dangling references when you do this. Can we capture by move? So when I say `[t=title]`, that copies `title`. That calls the copy constructor to copy `title` into the `t` data member of the lambda's captured state. Into the closure object. What if we wanted, not to call the copy constructor and make a second copy of the heap allocation, but instead to move it in? There is no shorthand for that. But we don't need shorthand. if I say `auto t = std::move(title);` that creates a `std::string t` and calls the move constructor to initialize it. So we just drop the `auto` again, and we get something like this: `[t = std::move(title)]`. That's how I would move a local variable into a lambda. And then I would not want to use the local variable again after that. So in that case, `t` is pilfering the pointer, stealing the guts from `title`. `title` becomes empty — or, goes into a valid unspecified state. `t` now gets its contents. And we didn't do an extra heap allocation there. We just used the move constructor. So there are many redundant shorthands for how to capture things. The one I've been showing— uh, the two I've been showing— Number one, `[t = title]`. Just think about that as putting `auto` on the front. You can also say `[&t = title]`. Just think about it as putting `auto` on the front. There are also these couple of shorthands that actually came in earlier. In C++11, these shorthands were all we had. You just say `[title]`. You name the variable in the outer scope. And this is roughly equivalent to saying `[title=title]`. Capturing a data member whose name is `title`, whose type is the same as the `title` from the outer scope. So it's similar to `[title=title]`. There are some little fiddly things going on here with array decay. Things going on with decltype, or how you use it in constexpr. They usually don't matter. That's why it's a shorthand. It is syntactic sugar. Still, for teaching purposes, I find that explaining it this way — "just stick the `auto` on the front" — that's the best way to communicate what's actually going on in this case. You can also use a very short shorthand to capture only what is needed. So if I put a single equals sign inside the square brackets, and nothing else, that means, "Capture only what is needed." "Look to see what is used in the body of the lambda. And anything that you don't recognize — anything that's not a parameter to the lambda, a local variable of the lambda —" "—Look it up in the outer scope. And if you find that it's a local variable of the outer scope, capture that." You can also say, "I capture only what I need — only what I use — but by reference. I capture references to all of those things." This is the most useful and most common kind of lambda: the kind of lambda that says, "I capture everything I need by reference." This is the kind of lambda that I'm going to make when I pass things down into standard algorithms. `std::sort`, `std::find_if`. Because I'm passing them down, it's safe to capture by reference. And I don't want to write out everything; I just want to keep it nice and short. There's a little caveat here. Globals and statics are not captured. Neither are unevaluated operands. Because they are not "needed." The address of a global doesn't change. To illustrate this, I'm going to show a little puzzle. Unfortunately, I'm not going to ask for hands. I'm just going to give you the answer. Here I have a main function — a program — And I've got global `int g = 10`. I then make two lambdas. I make `kitten` and `cat`. They both return `g+1`. The `cat` says, I capture a data member whose name is `g` and whose type is the same as that outer `g`. As if I had written `auto g=g`. It's a copy of `g`. The `kitten` says, I capture everything I need. Then in `main` I change the value of `g` from 10 to 20... And I ask the `kitten` and the `cat` for their results. What I see is different results. I see 21 and 11. The cat has captured a copy of `g`. So it captured a copy of 10. It initialized its data member to 10. And so it's returning 10+1, which is 11. The `kitten`, on the other hand, said, "I capture everything I need." And it doesn't NEED a copy of `g`. `g` is always available. It's a global. There is only one `g`; it's over there. `kitten` knows how to get to it when it needs it. And it doesn't need it until you call `operator()`. At which point it goes and looks at the value of `g`, and it sees that it's 20, and it adds 1 to it, and you get 21. So there is a subtle difference between saying "Capture everything I need"... ...and explicitly saying, "I would like a copy of this variable whether I need it or not." There are some other features of lambdas that are interesting, that are useful. One is that if I have a lambda which doesn't capture anything — which just has empty square brackets — These are also useful, because they are convertible to raw function pointers. I can take one of these lambdas and I can implicitly convert it to the type of its `operator()`. Well more or less. Its `operator()` is really a member function, right? So it doesn't have that type. But I can take the lambda and convert it to a pointer to a function that takes an int and returns an int. This is a handy way to define C-style functions in-line. And this is a very common idiom in some kinds of C++ code. We're actually going to see this being very important in my talk after lunch, "Type Erasure From Scratch," which will be at 1:30 PM over there in Aurora A. [ https://youtu.be/tbUCHifyT24 ] Another idiom you might see is that if I have one of these lambdas and I put a unary `+` in front of it... The unary plus is like the counterpart of unary minus. Unary minus says "negate the thing"; unary plus says "don't negate the thing." It's not a very useful operator. But what it does do is, it only works on scalar types. On arithmetic types, on pointer types. It doesn't work on class types. So when the compiler sees that you're trying to use it on this lambda type, which doesn't have an `operator+` defined, It says, "okay, I'm going to look for implicit conversions from this to something with an `operator+`." And what it finds is that conversion to the function pointer type. So it converts it to a function pointer type, and then "doesn't negate it." And so the result is that this call down here calls `fn` instantiated with a function pointer type, rather than with the lambda type itself. And so this can save on template instantiations. And we'll see more uses of that after lunch. Lambdas are also— well, they will be— default constructible. If I have a captureless lambda, then in C++20— this is a new change to enable even better metaprogramming and and things of that nature— if I have a lambda that all it does is add 1 to `x`, I will be able to say, "Get me the type of that lambda—" Right, I can't spell that type. It's spelled, like, `$_0`, `$_52`... I don't know how it's spelled... But I can say `decltype(lam)`, and that will give me the type. "–Declare a variable of that type and default-initialize it." When I default-initialize a captureless lambda in C++20, I will actually get an instance of that lambda type. It doesn't have any captures. There's nothing to copy. So it's fine to default-initialize it. So that's coming. So that may be useful. Lambdas are also `constexpr` by default in C++17. So I can actually use them inside static-asserts. Here I didn't make `lam` a constexpr variable, and I didn't have to write the word `constexpr` anywhere inside it. The compiler just made it constexpr by default, because it could be constexpr. If it tried to do something like throw, I believe it would not be constexpr. But the compiler can see that it's only doing constexpr things, and so it will make it callable in constexpr contexts like this. However, the compiler will NOT make it `noexcept`. Even though it can see that this won't throw, the call operator will not be marked `noexcept` by default. if you want it to be noexcept, you can put `noexcept` in the usual place — right here after the closing parenthesis. You can say, this lambda's call operator is `noexcept` for some reason. Generally, you don't have to do that. If you were just in Ben Saks' talk in this room before, I think he talked a little bit about `noexcept`. And generally, you're not going to need that. But in special cases you can put it in. Lambdas can also have local state. I can have a lambda that counts. But maybe not in the way that you think. I see that people sometimes will write code something like this `counter` is a lambda. It has an `operator()` that doesn't take any arguments, and it returns an int. We can tell that, by the way, because of the type of the expression in the return statement. You don't have to write the return type, but you can. You can put a trailing return type right here and say `-> int` if you want to. But you don't have to. So just like in a function, right? I have a function-local static; I'll make a lambda-local static. `static int i; return ++i;` And then I make a couple of counters It's just a class type. I can copy it. And then I start incrementing the one counter, and then after a while I start incrementing the other counter. And I find... ...that they share the same state! I get 1, 2, 3; and then 4, 5, 6 from the second counter! That's a little weird. If I had two functions, one named `c1` and one named `c2`, and they each had a `static int i` inside themselves, `c1`'s `i` and `c2`'s `i` would be different. But if I have two INSTANCES of the SAME lambda type, then that `static int i` is actually a static function-local variable inside the definition of `operator()` for that lambda type. That's a single function. There is a single `static int i`. All instances of the lambda type share that same operator, and share that same static. So, I see people trying to use `static` inside lambdas. Be careful. It may not do what you want. What did we want? Well, this is what we had, right? `static int i`. There's one `i`. `c1` and `c2` both refer to it. What we really wanted was for `c1` to have its own counter, and for `c2` to have its own counter. We already know how to do that, though! These are just class objects. If I want an object, an instance of a class, to have certain state, I put that as a data member. And how do I define a data member of a lambda? I put it in the square brackets. This lambda type, this lambda, has a data member named `i` initialized with 0. And what it does is increment its own `i`, its own data member `i`. Now we have two independent `i`s. Unfortunately, when we try to compile this we get a compiler error. Now, I'm showing you both GCC's error and Clang's error, to point out that Clang's error is better. GCC says, "Error. Increment of read-only variable `i`." This is weird, because we didn't say `const` anywhere. Clang says, "Cannot assign to a variable captured by copy in a non-mutable lambda." Which is more helpful, but also more jargon. What does it mean, "captured by copy in a non-mutable lambda"? Well, we didn't write `const` anywhere. But remember how I was emphasizing earlier that the `operator()` is always const, because we don't need to modify the object in order to call it? Well, the compiler is doing that for us. In a lambda I don't write `const`, but `operator()` becomes `const` anyway, by default Because that is sort of a sensible default. It should be `const`. Unfortunately, in this case I actually don't want the `operator() const`. I actually do want to modify it. I want to modify this object from within `operator()`, so it can't be `const`. How do I remove the `const` from an `operator()` where I didn't write `const` in the first place? I have to come up with some word that means, like, "negative const." Right? "-1 times const." And in C++, that word is `mutable`. So, for lambdas only, I can stick the keyword `mutable` right there, where I would normally stick the word `const` on a member function. Same place I would stick `noexcept`. And this negates the implicit `const`. So `mutable` doesn't actually change whether the data members themselves are const. They're usually not const. `i` here wasn't `const` to begin with. But the `mutable` negates the const-qualification of the lambda type's `operator()`. By default they're always const. Adding `mutable` makes it non-const. That means that you can't say "I want to modify this data member, but not that data member." It's all or nothing. Because it's not modifying the constness of the data members. It's modifying the constness of the call operator. Let's talk about something else. Generic lambdas. C++— Remember this slide? We're going to combine two features. C++ gives us member functions, like `Plus::plusme`. It also gave us templates. We can combine these together, and we can use member function templates. So `plusme` in this case is not a member function. It is a template for stamping out member functions. And when I have an object of type `Plus` here... I have my lowercase `plus` instance... And I can call `.plusme` of 42. That will do template type deduction and decide that it needs to stamp out a copy of the `plusme` member function that takes an `int`. `plus.plus me(3.14)` stamps out one that takes a `double`. And then I can combine this with operator overloading. I can change the name of the template from `plusme` to `operator()`. That's a call operator. That changes how it's called. I no longer have to write `plus.plusme`; I just open-parenthesis and I put the argument. Template type deduction kicks in and decides it needs to stamp out a copy of `operator()` that takes an `int`, a copy that takes a `double`... So now we can make something kind of nifty... We can take all the stuff in green... including that little bit of template... And we can collapse it all down and replace it with one pair of every kind of brackets... And we get something called a "generic lambda." Notice that our template parameter `T` has disappeared. Our template-head, `template<typename T>`, has disappeared. And instead, I'm just writing the word `auto`. This is special syntactic sugar. This is not necessarily obvious. But `auto` has something to do with type deduction... type inference... So here, when I write `auto` in the parentheses, it's not the same `auto` that you would see somewhere else The compiler is not going to deduce the type of this `auto` right here right now. This is just the shorthand indicating that this lambda's call operator, that takes an argument named `x`, is actually a template. And it will do deduction to figure out what the type of `x` should be... when it needs to. Which is when we call the call operator. When I have this object, and I call its call operator... (and I know you can't see that, because my mouse got too close to the bottom of the screen. Aagh. All right.) So I call its call operator. I say `plus(42)`. I pass 42 to `operator()`. The compiler says, 42 is an int. Therefore `T` must be `int` — the type of `x` must be an `int`. And it will deduce at that point. `plus` itself is not a template! Lowercase `plus` here is a variable. It's a variable of class type. What type? Well, we can't name it, but it's the equivalent of that capital-P `Plus` from the previous slide. I can make instances of that class. They are in no way templates. They just happen to have one member function template, as a member of the class. So generic lambdas are just templates under the hood A class type with a member function template. And you can do all the stuff with that that you could do with function templates. I can have variadic function templates. Here I have a variadic `operator()`. So this is a `plus` object that I can call with any kinds of arguments, and it will just add them all up. Variadic lambdas can reduce boilerplate. I take my capital-P `Plus` that looks like this... The irreducible complexity is in red. I collapse it all down — replace it with one pair of braces each. This is valid C++14. It's C++14 [not C++11] only because I'm using this longhand syntax to initialize `value` here. You can do the same thing in C++11. And now I have this object `plus` that I can call just as if it were a variadic function template. I can give it any arguments at all... ...and the compiler will statically know that it needs to instantiate the `operator()` of that `$_0` class. And it knows the argument types, and it will go stamp out a copy that does the right thing. Some other lambda trivia that I think is important to know, if you're going to be working a lot with lambdas... Number one: What does the `this` keyword mean inside a lambda? So, remember `has_title_t`, here? it captures a `std::string`. It has a data member named `t`. And inside the lambda I'm using that data member `t`. Now, inside a member function— I told you this was an `operator()` member function. And it is. Inside a member function, normally, when I refer to a data member, such as `t`, I can also access it explicitly by saying `this->t`. So you might think that it would work the same way in a lambda. That might be the intuitive thing. But that is not actually how it works! A lambda is syntactic sugar for one of these classes — `Plus`, `$_0`... But part of the syntactic sugar is that `this` means something different. Because at the source level we usually want the `this` keyword to expose something different from the lambda object itself. Because lambdas are always used in some context — in some larger context. Such as this class `Widget`. Here I have class `Widget`... It has a member function that goes and does some work, and then it has two public interfaces to doing that work. It has one that is synchronous. When you call `synchronous_foo()` with an `int x`, it calls `this->work(x)`. It also has an `asynchronous_foo()` that takes `x`, and... That's going to package up the operation of doing `this->work(x)` in a lambda, and it's going to fire it off to some other thread. If the `this` keyword inside a lambda meant the lambda object itself, and not the `Widget`, this wouldn't compile! Right? And we really do want this to compile. I don't want to have to think, when I'm refactoring my code: "Oh, I'm adding a lambda scope. I better go audit everything I just put inside the scope and change all the `this`s to some other thing." I just want this to work. And so, starting in C++11 when lambdas came in as part of the design... part of the design was, `this` doesn't mean the lambda object. It means whatever the keyword `this` means in that outer scope. So if I use a lambda inside a member function of `class Widget`, `this` refers to a `Widget`. And if I use a lambda in `main`, where `this` would be a syntax error, an ill-formed program, then it'll be a syntax error, an ill-formed program, to put it inside a lambda inside `main`. So it's good that these two `this`-expressions mean the same thing. It means we can reuse code snippets without counting the brackets so carefully. There are lots of ways of capturing `this`, just like there are lots of ways of capturing local variables. So the simplest way, and actually the way I showed on the previous slide, was to say "I capture everything that I need by value." That will also capture `this`, if `this` is used inside the lambda. `this` might also be used implicitly, by the way! When I wrote `this->work`, Since `work` was a member function of `Widget`, I could have actually left off the `this->`. That would be an implicit use of `this`, and it would have to capture the `this` pointer. So... let's see... So that — the capture by value — when that captures by value everything it needs and one of the things that it needs is the `this` pointer, it will capture the `this` pointer by value. This is often surprising. Because the `this` pointer... is a pointer. It's not a `Widget` object. This does not make a copy of the `Widget` object. This just captures a pointer by value. So this is surprising. People didn't really like this. And so that particular syntax— —The fact that when I say "I want everything by value," and then you use `this`— That will become deprecated in C++20, with the hope to remove it later. If you want to access `this`, because it has pointer semantics, you should capture it explicitly. Which you can do. Such as on the next line. I can say, "I'm capturing, explicitly, `this`." And I should mention you can also mix and match these I could say I'm capturing `[this,&]`. Or `[this,=]`. [Sadly the real syntax is only vice versa: `[&,this]` or `[=,this]`.] `[this,x,y]`. You know. I can mix these syntaxes inside that capture list. So I can explicitly capture `this`. That's a little bit of a special case. I can still say "I capture everything I need by reference." That will also capture the `this` pointer if it is needed. And that syntax is not being deprecated. Nobody is surprised that when I capture everything I need by REFERENCE, what I get is a pointer to the object — I don't actually capture the `Widget` object itself. This will continue to work. This is the best. You should always use this. If you use this, you will never use that deprecated syntax, either. New in C++17, you can capture `[*this]`. This is not something you can do with any other expression. I can't capture `[*p]` for any arbitrary `p`. But specifically for `*this`, that is special syntax that means I capture a copy of the `Widget` object itself. It's equivalent to having a local variable of that type, initialized with a copy of `*this`. Capturing `*this` by move has no shorthand equivalent. Again, you can just write it out longhand. That says— if I put the keyword `auto` in front of that, I would have `auto obj = std::move(*this)`. That means, move myself into that variable. That's also not something that I would expect to see in a lot of code, because it's moving-out-of the object whose member function I'm currently executing. So another question that might come up if you're using generic lambdas— Remember we took all the `T`s away? We didn't have `T` anymore. We just had `auto`. So how do I name that parameter type `T` inside the body of the generic lambda, if I need to? Well, here I have a variadic generic lambda. I have my generic lambda `plus`. Its `operator()` takes a variadic list of arguments of some type `Ts...` ...but I didn't say `T`... And it passes them all along by copy to this function `sum`. But we don't know how— well, maybe it's by reference. It doesn't do any special forwarding. On the other hand, if I'd tried to write perfect forwarding, which you learned about in some of the other Back to Basics talks this week, I take `auto&&`. Remember, I'm replacing `T` with `auto` here. So this is a forwarding reference. It's a pack of forwarding references, because I made this lambda variadic for no particular reason. But here I'm taking something of "deduced type ref ref." This is a forwarding reference. So now I have all these forwarding references. And I would like to forward them on to the `product` function. What do I write, inside these angle brackets to `std::forward`? Normally, this is where I would write `T`, or `Arg`, or whatever the name of the parameter type was. How do I name that type? (Since it doesn't have a name.) So there are at least two possible solutions. Number one: How do I name the type of `args`? Well, I use `decltype`! `decltype(args)`. And I pass that as the template parameter to `std::forward`. This works fine. You can do this. In C++20, we will also gain the ability to put... the fourth type of brackets. Now you can REALLY say you have one of every kind of brackets in your lambdas. Square brackets, angle brackets, round parens, curly braces. They're all there. Now it's a party. So I can actually say explicitly — just like I would in a template header — I can say, "I'm taking template parameters of type `Ts...`. Then I can use `Ts...` inside. And again, I made this all variadic for no reason. You can do the same thing with non-variadic generic lambdas as well. So that's coming. That's new in C++20. So another question I get about lambdas is, "So, are lambdas kind of like `std::function`?" Right? They both came in in C++11... `std::function` allows me to pass around— like, I pass a lambda to a function... it expects a `std::function`... it's happy with that... Why do we have both? What's the difference? And I'm going to defer that answer. If you have that question, I strongly encourage you to come to my next talk, "Type Erasure From Scratch," which will be over there. [ https://youtu.be/tbUCHifyT24 ] You'll be glad you went to this one. But `std::function` answers the question, "How do I write functions that accept lambdas as arguments?" One way to pass lambdas around — the STL way — The reason that lambdas fit so well into the older versions of C++ — with the STL — you know, you can pass a lambda to `std::sort`... The reason that worked so well is that the STL was already designed to make that work well. The STL is designed as a bunch of generic algorithms. Function templates. Those function templates take an arbitrary comparator, an arbitrary callback, an arbitrary predicate. So I can do that. I can copy what the STL does, if I want to pass around these lambdas. So I have my `Shelf` here. It has a member function `for_each_book`... But it's not a member function! It's a member function template. It's a template for stamping out member functions, each of which will be named `for_each_book`. But it'll take whatever type the user passes in. If the user passes in lambda type `$_0`, the compiler will stamp out a new member function called `for_each_book` that takes `$_0`. When the user passes in a different lambda type, it'll stamp out a different instantiation of this template. And for each one, this call right here, where I'm calling the call operator of that user type, the compiler knows statically, okay, I'm calling the call operator of that type. And it will generate the code, and it will be able to inline it... And this will get great codegen. Just a whole lot of it, if you use a lot of different lambda types. Here I'm calling `for_each_book`. I'm passing in a lambda type. Let's say this lambda type is `$_1`. What's going to happen is the compiler is going to stamp out a copy of `for_each_book` instantiated with `Func` — you know, `[with Func = $_1]`. And then here it's going to call the `operator()` of that lambda. So that means that this template definition — the definition of `for_each_book` where it has the call to that operator — That had better be visible in the same translation unit, in the same source file, as the definition of what the call operator does. There's no way to pass that lambda object across from one object file to another, using this method. That's why we always declare templates in header files. Right? So, alternatively, if you wanted to pass a lambda across an ABI boundary from one object file to another object file, you could do something like this. Here I have my class `Shelf`. It now has a concrete member function — not a template — that takes some concrete type. "Concrete callback type": `ConcreteCBType f`. And that concrete type has an `operator()` that does... something. Insert some indirection in there somehow, such that I can construct a `ConcreteCBType` object from any arbitrary lambda type. I don't pass the lambda. I use the lambda as the input to an implicit conversion, an implicit constructor, of `ConcreteCBType`. It's the `ConcreteCBType` object that gets passed over to the other object file. And then `ConcreteCBType::operator()` is going to do something that ends up invoking my lambda's call operator. We're going to see after lunch how that works, in my talk on type erasure. [ https://youtu.be/tbUCHifyT24 ] But this is where `std::function` becomes useful. `std::function` is a library type that implements type erasure, such that my `ConcreteCBType` here might just be a `std::function` of a certain signature. So that's the difference between a lambda and a `std::function`. `std::function` is a concrete type that allows you to pass things across boundaries. The lambda type itself, we've seen, is this very simple, statically typed, no heap allocation, no dynamic stuff going on at all. Right? It's all nice and static. So lambdas are... good. `std::function` are... some good, some bad. Do not come away from this thinking that if you use lambdas, you have to use `std::function`. You can very well use lambdas without necessarily having to pay for that type-erasure. You can also combine these two things, by the way. This would be something that you might see a standard library function doing. `std::thread`'s constructor, I believe, does this. To the user I say, "I have a template `for_each_book` that can take anything you pass me." I don't mention that type-erased callback type in my interface. I say, "any `F` at all, I will take it." "And I will do something with it that you don't have to know about." That's part of my implementation. But secretly what I'm going to do, inside, is I'm going to type-erase that lambda down to some concrete type and pass it off to the implementation, which can be declared in another file. So I might use this pattern, if I wanted to get that type-erased type out of my interface. And again, we'll talk more about type erasure later. But with that, we're getting close to the end here, I think. Oh, yeah, one more thing about lambdas. If you're going to use lambdas with `std::function`, eventually you're going to try to capture something inside a lambda that is not copyable. Here I have my lambda. `auto lamb` is an instance of a lambda type. That type has one data member, named `p`, whose type is `unique_ptr<int>`. So there's a data member of type `unique_ptr`. Lambdas always follow the Rule of Zero, by the way. Which means that if it contains a `unique_ptr`, it's not going to be copyable (but it is going to be movable). But since it's not copyable— So, here we show that it IS movable. I can move the lambda, and that will null out the `unique_ptr` inside `lamb`. Now `lamb2` has the new ownership of that `unique_ptr`. Just as it would with a class type. Write this out again— write it out in terms of `class Plus`. If you're ever confused what a lambda's doing, write it out as a class. Then you should be able to see how it works. However, when I try to make a copy, I find that there is no copy constructor of this lambda type, because it contains a move-only data member. So a lambda type can be copyable, movable, or neither; depending as its captures are copyable, movable, or neither. Whereas `std::function`, because of the way that they chose to design `std::function`... `std::function` can only hold things that are copyable. Because the `std::function` itself is copyable. Therefore there are some lambdas that can't be stored inside a `std::function`, inside a standard `std::function`. If I try to take the lambda — this move-only lambda — and move it into a `std::function`, I'm going to get a whole cascade of errors (that you're glad I'm not showing you) from deep inside the `std::function` implementation. Saying, essentially, you can't put a move-only lambda inside a `std::function`. So how do I fix that? One thing, that I think is really the best we can do for now, is: Place that lambda on the heap, and then share access to it from all the instances of `std::function`. I'm making it copyable by making a `shared_ptr` to it. And then you can copy that `shared_ptr`. And as long as you don't use the old one anymore, it doesn't really matter. So that's a little trick, if you have a lambda with move-only state inside itself. If it captures a `unique_ptr`. If it captures a `promise`. If it captures a `future`. All these move-only types in the C++11 standard library... You can use this `shared_ptr` trick to still put them inside a `std::function`, and get type erasure that way. The other solution is to use a move-only function type. `folly::Function` would be an example. There is a proposal, currently, for C++23, to introduce a `std::unique_function` that would be the same idea. You can write your own, if you come to my talk after lunch. I think every codebase needs a move-only function type. So if you don't have one, and you have a use for one, you know— Well, if you just have one place that uses the `shared_ptr` hack, that's fine. But if you find yourself doing it a lot, Come to my talk and write your own `unique_function`. And with that, we have about eight minutes left for questions. Thank you for coming. [APPLAUSE]
Info
Channel: CppCon
Views: 41,273
Rating: 4.9501557 out of 5
Keywords: Arthur O'Dwyer, CppCon 2019, Computer Science (Field), C++
Id: 3jCOwajNch0
Channel Id: undefined
Length: 52min 9sec (3129 seconds)
Published: Tue Oct 08 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.