Search code examples
c++c++11undefined-behaviorunique-ptrrvalue-reference

Why does operator* of rvalue unique_ptr return an lvalue?


Using the return value of operator* from a "dead" unique_ptr is bad.

The following code compiles but results of course in Undefined Behavior:

auto& ref = *std::make_unique<int>(7);
std::cout << ref << std::endl;

Why didn't the standard make the return type of operator* for an rvalue of std::unique_ptr an rvalue of the internal value, instead of an lvalue, like this:

// could have been done inside unique_ptr
T& operator*() & { return *ptr; }
T&& operator*() && { return std::move(*ptr); }

In which case this would work fine:

std::cout << *std::make_unique<int>(7) << std::endl;

But the code at the beginning would not compile (cannot bind an rvalue to an lvalue).


Side note: of course someone could still write bad code like the below, but it is saying "I'm UB" more verbosely, IMHO, thus less relevant for this discussion:

auto&& ref = *std::make_unique<int>(7);
std::cout << ref << std::endl;

Is there any good reason for operator* on an rvalue of std::unique_ptr to return an lvalue ref?


Solution

  • Good question!

    Without digging into the relevant papers and design discussions, I think there are a few points that are maybe the reasons for this design decision:

    1. As @Nicol Bolas mentioned, this is how a built-in (raw) pointer would behave, so "do as int does" is applied here as "do as int* does".

      This is similar to the fact that unique_ptr (and other library types) don't propagate constness (which in turn is why we are adding propagate_const).

    2. What about the following code snippet? It doesn't compile with your suggested change, while it is a valid code that shouldn't be blocked.

    class Base { virtual ~Base() = default; };
    class Derived : public Base {};
    void f(Base&) {}
    
    int main()
    {
        f(*std::make_unique<Derived>());
    }
    

    (godbolt - it compiles if our operator* overloadings are commented out)


    For your side note: I'm not sure auto&& says "I'm UB" any louder. On the contrary, some would argue that auto&& should be our default for many cases (e.g. range-based for loop; it was even suggested to be inserted automatically for "terse-notation range-based for loop" (which wasn't accepted, but still...)). Let's remember that rvalue-ref has similar effect as const &, extension of the lifetime of a temporary (within the known restrictions), so it doesn't necessarily look like a UB in general.