Hello and welcome to C++ Weekly. I'm your host, Jason Turner. In this episode I want to clear
up a misconception that I've heard several times, and you'll already know what that is
if you look at this episode title, but I'm going to demonstrate really what the differences are.
Now I know immediately that there's going to be some of you that are going to say,
"Well of course I knew that. Everyone knows that. Who doesn't know that?" Well, I can tell you lots
of people don't know this because a lot of people don't think about, or are aware of,
the intricate details for how these things are implemented in the C++ standard, and I see people
use these things interchangeably in code reviews and such, and so I just want to clarify that
a C++ lambda
is not standard function. These are two completely different topics. Now you can convert a lambda
into a function. I guess you could wrap a function with a lambda, but I am hard
pressed to immediately see a use case for that; but if we've got (and I'm going to make this just
a silly example here) let's say I have a lambda that can add two numbers.
Okay that's a lambda. Um... "unused variable." So let's just go ahead and
turn off -W error right now because we need this for the sake of demonstration
purposes here. All right. So we've got this function that's currently unused and I'm going
to go ahead and just return the value of "add 4+5" here, and we're going to see 9 is returned
from main. We're moving 9 into the register a0, fine. That's easy enough to reason about.
This is a lambda. Now I'm gonna take this back real quick and we're going to look at
an unoptimized build, and we're going to see a operator paren that has been created for us
in the lambda, and then we're going to see the operator paren is going to add,
well actually this is a little bit harder to read with no optimizations at all,
but the key here is going to be this bit. It's adding a1 and a0 and storing the results in a0.
So that's going to be y is a1, x is a0, the result is a0, and we're going to return a0
from the "add" function here. So it's relatively straightforward, but optimizations have been
disabled and it's generating 35 lines of assembly. If I turn on some optimizations I expect this to,
yes, completely go away again. Okay. So let's just look at the equivalent with a standard function.
You declare a function (std::function) like this,
um, but all I've declared is a holder to a function, so if I
were to try to do something like this (let's comment this out)...
Now if I try to call "func",
um, this, well, we can see right here it generates a call to throw_bad_function_call.
I haven't actually declared a function that does anything. This is a holder of a function. It is an
abstraction of a function, it is not a function by itself. So it is a, to be clear, a lambda.
So a std::function is a type erase wrapper around a callable, and a C++ lambda is a language
construct that helps us define an anonymous function. So this is generating a fair bit
of code for not actually doing anything, so let's just be nice about it. Let's go ahead and actually
assign this thing, our "add" lambda. So now it actually does something, and we can
see that even at -O1 the compiler has gotten very, very good at optimizing away the stuff.
I'm going to go ahead and put it in C++11 mode and we can see it's still optimizing
it away at -O1. Now let's, just for the fun of it, go back a few compiler versions. All right,
now we're at Clang 9, and at -O1 it is generating a considerable amount of code. The compiler hasn't
learned how to optimize away this thing yet. Now if I take it up to -O2 now it can. It adds 9 to 0
and returns that into a0. There we go. That's an interesting way of doing that, interesting risk
5 optimization or something there. Taking this back to a newer version of Clang -
yeah, it does just a copy into a0. Fascinating. Okay, so if we've got -O0
it has to generate all of this abstraction code and if we're really really lucky it
will optimize it away for us; and if we call the "add" function directly it's still having
to do all this work of setting this thing up even though we're not actually utilizing it,
and in the case of -O0 with the lambda it is just a fairly straightforward function call. So the
lambda is a function. std::function, arguably is not a function, it is a type erased wrapper around
a callable thing. Okay this is all very abstract I believe, so let's try to look at it another way.
I'm going to not actually do any work here, I'm going to create a function that calls a callable.
I'm going to use function pointer syntax for this in this case,
and I'm going to create a function called "add".
Okay. So this one takes a function pointer. Now this is the kind of thing that has been
built into the compiler since forever, and the code is exactly the same as what we were
looking at previously. Let's just add a little bit of optimization here to it. There we go, um,
so I can see the "add" function adds (oops x + 7, that's not what I meant to do - this is why
you should always have your warnings enabled and take them seriously), so x + y. It adds a1 and a0,
stores the result in a0, and then this function "call" - it's a function pointer. It knows that
it has a function pointer and it needs to call something that exists at a certain memory address.
Even at -O3 I generate the exact same code. Okay. Now I'm going to create a
different version of "call", and to someone who thinks of
these things as being equivalent maybe perhaps we'll see the difference here.
Okay, so this is at -O3 with an older compiler, but in this case I know that it's a simple
jump to whatever function was stored in a0, so a0 is going to be a function pointer, copies that
to a5, and then it's going to jump to a5
after setting up a0 and a1 the values 2 and 3 that are being passed to the function.
All right, and we can see approximately the same thing happening here - 2 and 3 being set up
onto the call stack, and then it is going to jump into some memory location here; and we have the
potential for a bad function call if the function object that was passed to us was empty, and this
is with optimizations turned all the way on. Let's take optimizations back a little bit and again we
can see that our version that takes a function pointer is going to set up some parameters to
"call", it's going to jump to whatever value. It's going to do its thing. Now that took (30)
19 instructions. All right, and then
this is everything that has to happen, and we can start to see some of this abstraction
that's happening, and we're going to talk about this in a future episode. So I think hopefully
I'm communicating correctly now the point that std::function gives us, and you're wondering,
"Okay if it has all this extra overhead then why does it even exist?" and it's because it can take
any callable kind of thing. So if I wanted to I can have, like, a vector of std::functions.
So I can have one that is an add, one that is a multiply, one that is a subtract and
that will all work. Now if you have been paying attention and already know something
about std::function and the abstractions that it provides and function pointers, then you might be
thinking, "Wait a minute, I can do the exact same thing if I use function pointers here."
Let's just go ahead and put on optimization so we can start to take a look at this.
That's -O3, and this is generating a fair bit of code. I mean it's a hand-wavy kind
of thing - "Oh! Look at all the code that it generated," but it's generating a fair bit of
code. Now remember - never, ever copy-paste, but I'm doing this for demonstration purposes.
In this case I could in fact
create a vector of function pointers. This is actually a perfectly allowed thing to do, and
it's helpful that a captureless lambda is implicitly convertible to a function pointer,
and I have so many episodes on lambdas that you should go and look at one of those, but I can
go ahead and also pass in an actual function pointer.
Now what this will not let me do is the more abstract case of saying I have something
like this. Let's say I want to capture a value and use this in my callable. I can't
do this in the function pointer case because it's only a captureless lambda that is convertible,
implicitly or explicitly, to a function pointer.
So let's go ahead and do this version, and we can see that the std::function is going to work
here now. It generates considerably more code of course, but this has a place. Sometimes you need
this very abstract behavior, you need the type erased thing, the std::function that is callable
on anything that happens to take that set of parameters. It's a lambda. It's a capturable
lambda. It's a... it's a function pointer, it's a member function pointer. It's anything - anything
that can be called with that set of parameters can be used in the place of a std::function,
and so I've decided that I'm going to go ahead and, for every episode that I can,
I'm going to go ahead and tie this back into the game engine that I introduced for the
"This Game Teaches C++ Episode." All right. So yes, very bright screen. Sorry, I know some of you
are going to complain about this. Why am I still getting, why am I, why do I have a build failure
here? Oh that's right, because code coverage is so low at the moment, but it's a work in
progress. So I actually have in my game logic... let's go ahead and look at game_components...
So for each location on the map I have a std::function. This is the kind of thing that
I would normally say, "Oh well, you know that's going to add more overhead than is necessary,"
but in this case it is not more overhead than is necessary. I wanted to be able to make this
as easy to program as possible and taking a std::function, being able to say, "Hey,
you know what, when you enter this tile I want you to execute this thing that takes
a reference to (a game) a point - that's the point, the location on the map that was entered,
and from which direction you entered the tile" so you can do whatever you want to there.
So it's very flexible to do it this way, and also all of my draw routines are std::functions. So
yeah, there's going to be a little bit of overhead where it's going to have to check and see if that
function is currently empty. If it's not empty then it has to jump to a location after setting up
the function parameters. Uh yeah, that's going to add more overhead but it's overhead
for the sake of flexibility in the game engine, and now this also gives me the possibility in
the future of very easily letting these be script functions. Like Chai Script would be perfect here
because a Chai Script function is convertible to a std::function. So I could easily plug Chai Script
in here if I wanted to, although, you know, just to be clear, I haven't maintained a Chai Script in
quite a while, other people have been maintaining it. Or I could use, you know, something like Sol2,
or Lua Bind or Pi Bind 11, or one of these things that can let you just get a std::function back
for a function that exists inside the scripting language, and this would be a perfect use case for
something like that. So yeah, I went a little bit deeper than I intended to, or maybe not as deep
as I could have, I'm not entirely sure here, but yeah, a lambda is not a std::function. A lambda is
a way of defining a callable object in C++. It's generated by the compiler for you. std::function
is a type erased wrapper around any kind of callable that gives you this generic interface
that you can pass around. It's more flexible than a function pointer. So maybe this episode will
be called something like "std::function versus lambda versus function pointer."
Maybe we'll call it something like that. So uh, thanks for watching this episode of C++
Weekly. I hope you enjoyed it. Be sure to subscribe and, you know, do the normal things.