Do you even RVO?
Written by Xander Bazzi on 01-25-25
Look at the code snippet below. How many times is MyType{} constructed? What about std::optional<MyType>?
std::optional<MyType> get_mytype(std::uint64_t some_val) {
if (!some_val) {
return std::nullopt;
}
return MyType{};
}
If you've used C++17's std::optional, you might have code that resembles the above. But is this the proper way of returning an optional? Let's reason about it:
1) We call the MyType default constructor
2) Then we call the optional copy constructor implicitly with the MyType value. Wait, but then are we constructing two objects? One std::optional and one MyType?
3) No, that can't be... We must be constructing the MyType r-value inside the optional. But then that means the std::optional had to be constructed prior...
After going throught these mental gymnastics, you might reach the conclusion that there are technically two constructions. And reasonably so, you might also assume that the compiler will optimize them both into a single in-place construction of an std::optional with the corresponding MyType object (somehow). Unfortunately, in true CPP fashion, this couldn't be further from the truth.
Let's replace our get_mytype function with something a little more useful. To help us get some insight into the object lifetime, let's write a function that returns a canonical Lifetime tracker:
std::optional<Lifetime> get_lifetime(std::uint64_t key)
{
if (key < 42ULL) {
return std::nullopt;
}
return Lifetime {};
}
And let's suppose we call it with:
auto main() -> int {
std::optional<Lifetime> a = get_lifetime(100ULL);
}
Which generates the following output:
1 Lifetime::Lifetime()
2->1 Lifetime::Lifetime(Lifetime&&)
2 Lifetime::~Lifetime()
1 Lifetime::~Lifetime()
Inspecting the result, we can see that there are two constructions in total (and two corresponding destructions):
- One default construction of the unnamed Lifetime from the get_lifetime() function
- And another move construction by moving that temporary into a in the main() (caller) context
I know what you're thinking: "the compiler will optimize both of these into one construction". And you'd be correct if the
Lifetimeclass had a default constructor. But it doesn't, and neither do most classes. Here's a good Jason Turner video on RVO.
You probably expected C++17's guaranteed copy-elision (for unnamed RVO) to completely skip the construction inside get_lifetime() and instead just construct it at the caller site. The only problem is that URVO is only guaranteed when the return type exactly matches the caller type. In our case, a Lifetime is not an std::optional<Lifetime>, so we need to first construct the Lifetime and call the std::optional<Lifetime> constructor (which moves or copies the Lifetime).
To tell std::optional that we want to construct a Lifetime in-place, we have to use std::in_place; bet you didn't see that one coming. Here's the revised correct copy-elided version that leverages URVO:
std::optional<Lifetime> get_lifetime(std::uint64_t key)
{
if (key < 42ULL) {
return std::nullopt;
}
return std::optional<Lifetime> {std::in_place};
}
Now we get the expected behavior: the Lifetime constructor is called once.
1 Lifetime::Lifetime()
1 Lifetime::~Lifetime()
Yes, there is an std::optional parameterized constructor that takes in an std::in_place. Same thing goes for std::expected, std::tuple, std::pair, and probably other STL wrappers.

Another C++ idiosyncracy to be aware of. On to the next one.