Why do I need to move `std::unique_ptr`
Asked Answered
K

2

9

Given the following code:

#include <iostream>
#include <memory>

struct A {};

struct B : public A {};

std::pair<bool, std::unique_ptr<B>> GetBoolAndB() {
    return { true, std::make_unique<B>() };
}

std::unique_ptr<A> GetA1() {
    auto[a, b] = GetBoolAndB();
    return b;
}

std::unique_ptr<A> GetA2() {
    auto [a, b] = GetBoolAndB();
    return std::move(b);
}

GetA1 does not compile, with this error:

C2440: 'return': cannot convert from 'std::unique_ptr<B,std::default_delete<_Ty>>' to 'std::unique_ptr<A,std::default_delete<_Ty>>'

while GetA2 does compile without errors.

I don't understand why I need to call std::move to make the function work.

Edit

Just to clarify, as pointed out in comments by DanielLangr, my doubt was about the fact that

std::unique_ptr<A> GetA3() {
    std::unique_ptr<B> b2; 
    return b2;
}

compiles and transfer ownership without the need for std::move.

Now I understand that in case of GetA1 and GetA2, with structured bindings it happens that b is part of some object, and so it must be moved to become an rvalue reference.

Kiangsu answered 13/8, 2021 at 9:8 Comment(6)
I add another observation: if in GetA1 you add std::unique_ptr<B> b2; and return b2; instead of return b;, then the code compiles. So the problem seems to be related to the structured binding. On the other hand, static_assert(std::is_same_v<decltype(b), decltype(b2)>); passes, so I really don't know what's the difference between returning b or another lobcal object of the same type.Dissatisfactory
Are you asking why std::unique_ptr needs to be moved in general, i.e. why it’s non-copyable, or are you asking why it needs to be moved in this specific situation?Doglike
Think of it like this, a unique pointer denotes unique ownership. There can be only one such owner. If you return a unique_ptr then you are basically saying: the code that calls GetA2 will become the new owner of the unique pointer and you have to move ownership, hence the moveMarshmallow
@PepijnKramer I think you don't understand the problem OP is asking for. If you write std::unique_ptr<B> b2; return b2;, it will compile and transfer ownership without the need for std::move. I believe OP is asking why it does not work the same way with structured bindings.Tertia
@DanielLangr Oops, thanks. I misread the question.Marshmallow
@DanielLangr exaclty :) in fact your answer is great, thank you!Kiangsu
T
5

I don't understand why I need to call std::move to make the function work.

Because the corresponding constructor of std::unique_ptr has a parameter of rvalue reference type:

template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u ) noexcept;

See documentation for details: https://en.cppreference.com/w/cpp/memory/unique_ptr/unique_ptr

Since rvalue references cannot bind lvalues, consequently, you cannot use b (which is lvalue) as an argument of this constructor.

If you wonder why b is treated as lvalue in the return statement, see, for example: Why Structured Bindings disable both RVO and move on return statement? In short, b is not a variable with automatic storage duration, but a reference to a pair element instead.

The error message basically just says that the compiler could not find any viable converting constructor, therefore, it "cannot convert...".

By wrapping b with std::move call, you are creating an expression that refers to the very same object as b, but its category is rvalue. Which may be bound with that constructor parameter.

Tertia answered 13/8, 2021 at 9:14 Comment(4)
Hi, would you mind reading my comment under the question. It looks like returning a local lvalue isn't a problem in and by itself.Dissatisfactory
@enlico It very much depends on what that lvalue actually is. I updated the answer with the link to the relevant post. Note that b here is not a variable with automatic storage duration.Tertia
But in the example I made b and b2 have same type and value category; doesn't the static_assert ensure that? How can the compiler behave differently with the two of them?Dissatisfactory
@enlico Because decltype for structured bindings removes reference, that is, it resolves to the referenced type: en.cppreference.com/w/cpp/language/structured_binding.Tertia
A
0

Because there should only be one valid unique_ptr at any one time. That is why it is called unique_ptr.

unique_ptr is un-copyable, you must move it.
Otherwise you would end up with a copy of the pointer which would defeat the point of it being unique!
See: Rules for Smart Pointers

Avellaneda answered 13/8, 2021 at 9:28 Comment(8)
That's not entirely true here. The compiler is allowed to perform NRVO in certain circumstances, and in fact you should avoid std::move when returning a unique pointer, see e.g. this question. The OP's case is a bit special though, as pointed out in Daniel Langr's answer.Badly
@Badly Note that the problem is not at all about NRVO, which is always optional. The problem is about treating the named object in return statement as rvalue under some circumstances. And this is mandatory if they are met. (NRVO just says that copy/move constructors are elided, while the relevant rule says that move constructor should be applied instead of copy constructor. Both problems are orthogonal.)Tertia
@DanielLangr I was referring to "you must move it" wrt. returning a unique_ptr. It's a common misconception that people believe they always have to move, just because it's a unique_ptr. If you're simply returning the pointer, that's not true.Badly
@Badly I agree. Just wanted to clarify that NRVO thing.Tertia
I think you don't understand the problem OP is asking for. If you write std::unique_ptr<B> b2; return b2;, it will compile and transfer ownership without the need for std::move. I believe OP is asking why it does not work the same way with structured bindings.Tertia
@DanielLangr since the OP has accepted your answer and not mine, you are clearly correct. However, my answer is to the title to the OP's question, which is far more general than his specific issue.Avellaneda
@Avellaneda I understand, but, generally, you cannot write answers only according to the post titles. Though OP could have mentioned structured bindings there in this case. Without it, the title is too generic.Tertia
Agreed @DanielLangr. If Dundo would be kind enough to edit the title of their question to be more specific, I'll happily delete this answer.Avellaneda

© 2022 - 2024 — McMap. All rights reserved.