Why does operator* of rvalue unique_ptr return an lvalue?
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:
As @Nicol Bolas mentioned, this is how a built-in (raw) pointer would behave, so "do as
int
does" is applied here as "do asint*
does".This is similar to the fact that
unique_ptr
(and other library types) don't propagateconst
ness (which in turn is why we are addingpropagate_const
).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.
Your code, in terms of the value categories involved and the basic idea, is the equivalent of this:
auto &ref = *(new int(7));
new int(7)
results in a pointer object which is a prvalue expression. Dereferencing that prvalue results in an lvalue expression.
Regardless of whether the pointer object is an rvalue or lvalue, applying *
to a pointer will result in an lvalue. That shouldn't change just because the pointer is "smart".
std::cout << *std::make_unique<int>(7) << std::endl;
already works as the temporary dies at the end of the full expression.
T& operator*() & { return *ptr; }
T&& operator*() && { return std::move(*ptr); }
wouldn't avoid the dangling reference, (as for your example)
auto&& ref = *std::make_unique<int>(7); // or const auto&
std::cout << ref << std::endl;
but indeed, would avoid binding a temporary to a non-const lvalue reference.
Another safer alternative would be:
T& operator*() & { return *ptr; }
T operator*() && { return std::move(*ptr); }
to allow the lifetime extension, but that would do an extra move constructor not necessarily wanted in the general case.