Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 208
Amir Kirsh Avatar asked Jul 24 '19 14:07

Amir Kirsh


People also ask

What happens when unique_ptr goes out of scope?

unique_ptr. An​ unique_ptr has exclusive ownership of the object it points to and ​will destroy the object when the pointer goes out of scope.

What does unique_ptr mean in C++?

std::unique_ptr is a smart pointer that owns and manages another object through a pointer and disposes of that object when the unique_ptr goes out of scope. The object is disposed of, using the associated deleter when either of the following happens: the managing unique_ptr object is destroyed.

What happens when you move a unique_ptr?

A unique_ptr can only be moved. This means that the ownership of the memory resource is transferred to another unique_ptr and the original unique_ptr no longer owns it.

What does unique_ptr get do?

std::unique_ptr::getReturns the stored pointer. The stored pointer points to the object managed by the unique_ptr, if any, or to nullptr if the unique_ptr is empty.

What is the difference between an rvalue and an lvalue reference?

Now an lvalue reference is a reference that binds to an lvalue. lvalue references are marked with one ampersand (&). And an rvalue reference is a reference that binds to an rvalue. rvalue references are marked with two ampersands (&&).

What is rvalue in C++?

The term rvalue refers to a data value that is stored at some address in memory. An rvalue is an expression that cannot have a value assigned to it. Both a literal constant and a variable can serve as an rvalue. When an lvalue appears in a context that requires an rvalue, the lvalue is implicitly converted to an rvalue.

What is *PTR in C language?

For example, if ptr is a pointer to a storage region, then *ptr is a modifiable l-value that designates the storage region to which ptr points. In C, the concept was renamed as “locator value”, and referred to expressions that locate (designate) objects. The l-value is one of the following:

Can lvalue const references bind to rvalues?

I mentioned that lvalue const references could bind to rvalues: void f (MyClass const& x) { ... } but they are const, so even though they can bind to a temporary unnamed object that no one cares about, f can’t modify it.


3 Answers

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".

like image 108
Nicol Bolas Avatar answered Oct 24 '22 05:10

Nicol Bolas


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.

like image 35
Jarod42 Avatar answered Oct 24 '22 03:10

Jarod42


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.

like image 4
Yehezkel B. Avatar answered Oct 24 '22 05:10

Yehezkel B.