Hello and welcome to C++ Weekly. I'm your
host Jason Turner. I'm available for code reviews and
on-site training. This episode intentionally
has a click-baity title because I wanted to get your
attention and thinking more about object
lifetime, but before we get to that, I would like to point
out that I am again giving a C++ Best Practices class at NDC Techtown in Kongsberg, Norway this
fall on Monday, September 9th, and
Tuesday, September 10th. I had a great turnout in the
class last year and I would love to see you
all join me again this year for this Best Practices
class. Okay, so to help illustrate the point, I
have again created one of my little lifetime helpers.
If you're a fan of this channel, you have seen
this or something like it many times in the past,
although I have upgraded this to be C++20-ified, so I just have a handy little print function here
that I call that automatically prints the
current name of the function that we are executing.
The title of this episode says you are
using tuple, pair, variant, optional, expected wrong,
and I'm just going to focus on optional because it was really the first thing that
made me aware of this particular issue.
Now I'm going to return an optional object
from this function and the point is that this issue applies to every single one of our kind
of, algebraic holder type things that we have
in the C++ standard, and it's not just the ones that
are in the standard, it's also probably ones that you have created yourself.
Okay, so I'm just going to return an optional
that is empty here and I am printing the output from
this program. We can see that absolutely nothing is printed here and that's because I
didn't populate this optional with a value,
so I'm going to go ahead and I'm going to create one.
This is my optional and now I'm going to
return it and I'm still not going to get anything printed
because I still haven't actually put anything in this optional yet. So now I'm going to
say that I want to populate this thing and
what you'll notice here is I get the object constructed
on line 28. Then this assignment is
actually assigning the optional value itself, and then
I'm getting a move constructor because I am assigning it with the result of a temporary.
I've just made the situation worse
because now I am getting a copy construction on that
assignment instead of a move construction, but either way, I have two objects that have existed
here. If I go back to the version that has a move
constructor, I have the one that was created here,
and then I have the move that results during this assignment operation. "Okay," you say,
"Why am I even creating this local retval? Why don't I just return something like this?" and
you'll see that I am still getting a value
that was created with an integer and then I am getting
a move construction happening. I still have two of these objects, and this is the fundamental
problem with these things. If I want to
actually create that thing in place, then I have to make
sure that I am calling emplace, the function, on
the container, or I am calling the proper constructor
that creates the thing in place, or I'm using in_place_t to make sure that I'm calling the thing
in place. Either way, this just doesn't ever do what we really want it to do. The kind of
thing that we might, you know, default to would be creating a local and just returning that
and we're still going to get that construction and then that move operator so
we break return value optimization all over the place. Again, this isn't just about optional -
this applies to expected; it applies to variant. It applies to tuple; it applies to pair,
and getting this right is actually kind of a pain. So let's look at our options.
In this version, I am actually constructing the thing in place in the optional and I can see
that I get exactly one lifetime object
created, the one that takes an int parameter, and then its
destructor is called, but this is rather cumbersome. What we really want to be able to do
is just return the thing. Okay, so the question
is, how do we get the return value optimization that
we want with only one object that we're trying to return actually being constructed?
It's kind of a pain. This interestingly
actually does what we want it to do, where I am explicitly
constructing an object of the optional type and returning that. Now, if you recall, if I
explicitly create a lifetime object, then I
have to go through that move constructor, which is not
what I want. If I do this, then I am calling
the constructor for optional that takes a parameter
that is convertible to an object of lifetime type.
Now let's go ahead and make this explicit and see
if we change the meaning of this code at all because as we know, we should have single
parameter constructors marked as explicit so that we don't get too many accidental implicit
conversions in our code, but that breaks this code
again. So this is an option. We can do this. We can
also do the version that takes an in_place_t. So let's go ahead and try that. Now I'm explicitly
telling the library that I want to construct
this thing in place and I want to do that by calling
the constructor for lifetime that takes a integral. Can I now do this in the braced
initialization? I still cannot because this
is an explicit constructor that takes the in_place_t.
Okay, it appears that some version of this is
the thing that we are stuck with. Why do I bring this
up? I bring this up specifically because of expected. I know lots of people like to
think that they are using optional correctly and aren't going to run into these problems,
but expected is now supposed to be, like, the more efficient non-exception way that we can return
errors in C++23. Now I need to give it a lifetime, which is the expected value,
or an optional error type, and let's just give it a string_view for an error description.
So I propose that we're going to see lots of code that looks like this,
and we are falling right back into this case where we're actually doing something that, in
many cases, is going to be less efficient than just
using exceptions, especially in the case where we
actually think that this is going to be an exceptional thing to do. So if you really want
to do this kind of thing and do it
efficiently in a way that can take advantage of return value
optimization, then you actually have to
create an object of this type initialized with the value
that you want it to be initialized with and return
that, or if you do this in a case where you really
expect that return value optimization is going to come into play and it doesn't, you basically have
to do this to get the return value optimization
that you want. This is just all very unfortunate
in my mind and makes us think much too hard about these things and I largely blame the standard
library. This problem is not a problem that
the standard library can directly solve. Where I blame
the standard library is the fact that there
does exist implicit conversions that allow this code
to compile at all. I would argue this code
should not compile because we have broken something that
has been considered best practice for well
over 20 years in the C++ community and that is that
single parameter constructors should be
marked explicit. This works because there is an implicit
conversion from an object of type lifetime to an object of expected of lifetime. So to reiterate,
I largely do blame the standard library
designers for adding more and more explicit conversions
between these types and making it look like
we have return value optimization in cases where we
don't and it's difficult to impossible to
catch all of these tiny micro-pessimizations that you're
going to get throughout your code base. Just
in case you don't believe me, you can see here that
I do have an extra destructor that must be
called for the moved from object because we don't have
destructive moves in C++. So anyhow, I just
want you to, I don't know, regret watching this episode
and wonder how in the world you're supposed to take these things into account.
So thanks for watching and be sure to subscribe.