Search code examples
c++c++17unique-ptrmove-semanticsstructured-bindings

Why do I need to move `std::unique_ptr`


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.


Solution

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