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.

Dundo
  • 714
  • 8
  • 12
  • 1
    I add another observation: if in `GetA1` you add `std::unique_ptr 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);` passes, so I really don't know what's the difference between returning `b` or another lobcal object of the same type. – Enlico Aug 13 '21 at 09:22
  • 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*? – Konrad Rudolph Aug 13 '21 at 09:26
  • 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 move – Pepijn Kramer Aug 13 '21 at 09:40
  • @PepijnKramer I think you don't understand the problem OP is asking for. If you write `std::unique_ptr 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. – Daniel Langr Aug 13 '21 at 09:42
  • @DanielLangr Oops, thanks. I misread the question. – Pepijn Kramer Aug 13 '21 at 09:45
  • @DanielLangr exaclty :) in fact your answer is great, thank you! – Dundo Aug 13 '21 at 09:45

2 Answers2

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.

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • 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. – Enlico Aug 13 '21 at 09:23
  • 1
    @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. – Daniel Langr Aug 13 '21 at 09:24
  • 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? – Enlico Aug 13 '21 at 09:26
  • 1
    @enlico Because `decltype` for structured bindings removes reference, that is, it resolves to the referenced type: https://en.cppreference.com/w/cpp/language/structured_binding. – Daniel Langr Aug 13 '21 at 09:32
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

kenba
  • 4,303
  • 1
  • 23
  • 40
  • 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](https://stackoverflow.com/questions/4316727/returning-unique-ptr-from-functions) question. The OP's case is a bit special though, as pointed out in Daniel Langr's answer. – andreee Aug 13 '21 at 09:32
  • @andreee 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.) – Daniel Langr Aug 13 '21 at 09:34
  • @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. – andreee Aug 13 '21 at 09:37
  • @andreee I agree. Just wanted to clarify that NRVO thing. – Daniel Langr Aug 13 '21 at 09:39
  • 1
    I think you don't understand the problem OP is asking for. If you write `std::unique_ptr 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. – Daniel Langr Aug 13 '21 at 09:44
  • @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. – kenba Aug 13 '21 at 09:53
  • 1
    @kenba 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. – Daniel Langr Aug 13 '21 at 10:00
  • 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. – kenba Aug 13 '21 at 10:19