c++c++23return-value-optimization

Where is the first destructor being called here?


In the expected_func_2() function, we see two variations, which can be seen here on Godbolt. In the first example, we only have one constructor and one destructor called, this is because RVO is being utilized. In the second example, where the value is constructed with emplace.

#include <iostream>
#include <expected>
#include <string>

struct Verbose {
    Verbose() {
        std::cout << "Default constructor\n";
    }
    explicit Verbose(int i) noexcept {
        std::cout << "Int constructor\n";
    }
    Verbose(const Verbose&) {
        std::cout << "Copy constructor\n";
    }
    Verbose(Verbose&&) {
        std::cout << "Move constructor\n";
    }
Verbose& operator=(const Verbose&)
    {
        std::cout << "Copy assignment\n";
    }
    Verbose& operator=(Verbose&&)
    {
        std::cout << "Move assignment\n";
    }
    ~Verbose() {
        std::cout << "Destructor\n";
    }
};


auto expected_func() -> std::expected<Verbose, std::string>
{
    if (5 > 10) {
        return std::unexpected("math is broken");
    }
    return std::expected<Verbose, std::string> { std::in_place, 7 };
}

auto expected_func_2() -> std::expected<Verbose, std::string>
{
    std::expected<Verbose, std::string> ret;
    if (5 > 10) {
        // This is needed to have RVO:
        ret = std::unexpected("math is broken");
        return ret;
    }
    ret.emplace(7);
    return ret;
}

//...
int main() {
    {
    std::cout << "Using in_place:\n";
    auto out1 = expected_func();
    //auto verb1 = std::move(*out1);
    }
    {
    std::cout << "\nUsing emplace:\n";
    auto out1 = expected_func_2();
    //auto verb1 = std::move(*out1);
    }
}

The GCC ouput is:

Using in_place:
Int constructor
Destructor

Using emplace:
Default constructor
Destructor
Int constructor
Destructor

I understand in the emplace version that the default constructor is being called when:

std::expected<Verbose, std::string> ret;

is being constructed.

Then, there is the first call to the destructor, which I don't understand what's causing that. Then there is 'int constructor', which is what happens when:

ret.emplace(7); 

happens.

And then, the final destructor is at the end of the { } scope in the main() function.

I don't understand where the first destructor call is happening. It can't be a copy being constructed and destructed inside the function, because there is no copy or move happening, meaning there is RVO happening, so I don't get why there is the first destructor call.


Solution

  • ret holds a value, it's not "empty" like an optional. Since you let it be default initialized, a value (and not an unexpected obj) is default initialized. In fact, the default c'tor is constrained on T (the expected type) being default constructible.

    When you emplace a new value into it, the old "expected" Verbose needs to be replaced (by having its lifetime terminated, and storage reused).

    Essentially, the expected does something like:

    _M_value.~Verbose(); // what you see printed
    new (&_M_value) Verbose(i);
    

    By the same token, if ret held an unexpected error object, that would need to be destroyed before emplacing anything into the object storage.