How does returning std::make_unique work

2020-07-05 06:28发布

问题:

I have a base class and its subclass:

class Base {
    public:
    virtual void hi() {
        cout << "hi" << endl;
    } 
};

class Derived : public Base {
    public:
    void hi() override {
        cout << "derived hi" << endl;
    } 
};

Trying to create a helper function that creates a unique pointer of a Derived object.

1) This one works:

std::unique_ptr<Base> GetDerived() {
    return std::make_unique<Derived>(); 
}

2) But, this one fails to compile:

std::unique_ptr<Base> GetDerived2() { 
    auto a = std::make_unique<Derived>(); 
    return a; 
}

3) std::move works:

std::unique_ptr<Base> GetDerived3() {
    auto a = std::make_unique<Derived>();
    return std::move(a); 
}

4) If I create a Base instance, both work:

std::unique_ptr<Base> GetDerived4() {
    auto a = std::make_unique<Base>();
    return a; 
}

std::unique_ptr<Base> GetDerived5() {
    auto a = std::make_unique<Base>();
    return std::move(a); 
}

Why (2) fails but others work?

回答1:

std::unique_ptr is not copyable, only movable. The reason you can return std::make_unique<Derived> from a function declared to return std::unique_ptr<Base> is that there is a conversion from one to the other.

So 1) is equivalent to:

std::unique_ptr<Base> GetDerived() {
    return std::unique_ptr<Base>(std::made_unique<Derived>());
}

Since the value returned from std::make_unique is an rvalue, the return value is move-constructed.

Contrast that to 2), which is equivalent to:

std::unique_ptr<Base> GetDerived2() { 
    std::unique_ptr<Derived> a = std::make_unique<Derived>(); 
    return std::unique_ptr<Base>(a); 
}

since a is an lvalue, the return value must be copy-constructed, and std::unique_ptr is non-copyable.

3) works because you cast the lvalue a to an rvalue, and the return value can be move-constructed.

4) and 5) work because you already have a std::unique_ptr<Base> and don't need to construct one to return.



回答2:

std::unique_ptr<> has no copy constructor, but it does have a move constructor from a related pointer, i.e.

unique_ptr( unique_ptr&& u );         // move ctor
template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u );   // move ctor from related unique_ptr

The second constructor requires certain conditions (see here). So why did your code 2 not work, but 4 did? In 4, you didn't use any constructor, since the return type was identical to the object, the object itself was returned. In 2 on the other hand, the return type was different and a constructor call was needed, but that required std::move().



回答3:

In every case but (2) the returned value was treated as (some kind of) rvalue. In (2) it was not, because the types did not match the implicit move was blocked.

In a later iteration of the standard, (2) would also implicitly move.

All of them shortly engage in undefined behaviour after being called, as they try to delete a Derived object via pointer-to-Base. To fix this, record a deleter function.

template<class T>
using smart_unique=std::unique_ptr<T, void(*)(void*)>;

template<class T, class...Args>
smart_unique<T> make_smart_unique( Args&&... args ){
  return {
    new T(std::forward<Args>(args)...),
    [](void*ptr){ delete static_cast<T*>(ptr); }
  };
}
template<class T>
static const smart_unique<T> empty_smart_unique{ nullptr, [](void*){} };

These are unique pointers smart enough to handle polymorphism like shared_ptr can.



回答4:

In the above listed example, (1) returns an rvalue but (2) is not an rvalue and is attempting a copy on the unique_ptr, which you cannot do for a unique_ptr.

Using move works because you are treating the unique_ptr at that point as an rvalue.



回答5:

You will find that std::unique_ptr can move from derived's to base's, if you look into its definition. Basically is_convertible here check this situation.

   /** @brief Converting constructor from another type
   *
   * Requires that the pointer owned by @p __u is convertible to the
   * type of pointer owned by this object, @p __u does not own an array,
   * and @p __u has a compatible deleter type.
   */
  template<typename _Up, typename _Ep, typename = _Require<
           __safe_conversion_up<_Up, _Ep>,
       typename conditional<is_reference<_Dp>::value,
                is_same<_Ep, _Dp>,
                is_convertible<_Ep, _Dp>>::type>>
unique_ptr(unique_ptr<_Up, _Ep>&& __u) noexcept
: _M_t(__u.release(), std::forward<_Ep>(__u.get_deleter()))
{ }