Skip to content

C++ Weekly – Ep 421 – You’re Using optional, variant, p… — Transcript

Jason Turner explains common misuse of C++ standard library types like optional and expected, focusing on object lifetime and return value optimization.

Key Takeaways

  • Returning local optional or expected objects by value often causes extra copies or moves, hurting performance.
  • Using emplace or in_place_t to construct objects in place can avoid unnecessary object lifetime overhead.
  • Implicit conversions in the standard library can lead to unexpected inefficiencies and should be avoided.
  • Explicit single-parameter constructors are preferable to prevent accidental implicit conversions.
  • Understanding object lifetime and return value optimization is crucial when using modern C++ standard library types.

Summary

  • Jason Turner discusses misuse of C++ standard library types such as optional, variant, pair, tuple, any, and expected.
  • The episode focuses primarily on optional to illustrate issues with object lifetime and return value optimization (RVO).
  • Returning local objects wrapped in these types often leads to unnecessary copy or move constructions, breaking RVO.
  • Constructing objects in place using emplace or in_place_t can avoid extra copies but is cumbersome.
  • Implicit conversions and non-explicit single-parameter constructors in the standard library contribute to inefficiencies.
  • Expected, introduced in C++23 for error handling, suffers from similar issues as optional regarding object lifetime and efficiency.
  • The standard library's allowance of implicit conversions can cause subtle performance degradations that are hard to detect.
  • Jason criticizes the standard library design for making it difficult to achieve efficient code with these types.
  • He advocates for explicit construction and returning objects directly to maximize RVO benefits.
  • The video encourages developers to rethink how they use these algebraic data types to avoid micro-inefficiencies.

Full Transcript — Download SRT & Markdown

00:00
Speaker A
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<lifetime> 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 an 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.
00:17
Speaker A
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
00:35
Speaker A
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.
00:46
Speaker A
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
01:00
Speaker A
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
01:18
Speaker A
made me aware of this particular issue. Now I'm going to return an optional<lifetime> 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
01:36
Speaker A
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.
01:47
Speaker A
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,
02:01
Speaker A
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
02:17
Speaker A
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.
02:44
Speaker A
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
02:59
Speaker A
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
03:16
Speaker A
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
03:34
Speaker A
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
03:58
Speaker A
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
04:21
Speaker A
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.
04:42
Speaker A
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
05:01
Speaker A
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?
05:17
Speaker A
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
05:35
Speaker A
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.
05:53
Speaker A
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
06:06
Speaker A
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
06:25
Speaker A
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.
06:45
Speaker A
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,
07:07
Speaker A
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.
07:38
Speaker A
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
07:57
Speaker A
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
08:14
Speaker A
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
08:38
Speaker A
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
09:02
Speaker A
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
09:21
Speaker A
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,
09:42
Speaker A
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
10:04
Speaker A
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
10:19
Speaker A
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.
10:29
Speaker A
So thanks for watching and be sure to subscribe.
Topics:C++optionalexpectedvariantpairtupleobject lifetimereturn value optimizationC++20C++23

Answers

Frequently Asked Questions

Why does returning an optional object by value often cause extra copies or moves?

Returning an optional by value typically involves creating a local object and then moving or copying it when returning, which breaks return value optimization and results in multiple object constructions.

How can I avoid unnecessary copies when returning optional or expected objects?

You can avoid extra copies by constructing the contained object in place using emplace or in_place_t, or by returning an explicitly constructed optional or expected object initialized with the desired value.

What is Jason Turner's criticism of the C++ standard library regarding these types?

He criticizes the standard library for allowing implicit conversions and non-explicit single-parameter constructors, which lead to subtle performance issues and make it difficult to achieve efficient return value optimization.

Get More with the Söz AI App

Transcribe recordings, audio files, and YouTube videos — with AI summaries, speaker detection, and unlimited transcriptions.

Or transcribe another YouTube video here →