Search code examples
c++initializationc++17implicit-conversionunique-ptr

Can't use copy-initialization with multiple steps of implicit conversions


I am having trouble understanding why the following copy-initialization doesn't compile:

#include <memory>

struct base{};
struct derived : base{};

struct test
{
    test(std::unique_ptr<base>){}
};

int main()
{
    auto pd = std::make_unique<derived>();
    //test t(std::move(pd)); // this works;
    test t = std::move(pd); // this doesn't
}

A unique_ptr<derived> can be moved into a unique_ptr<base>, so why does the second statement work but the last does not? Are non-explicit constructors not considered when performing a copy-initialization?

The error from gcc-8.2.0 is:

conversion from 'std::remove_reference<std::unique_ptr<derived, std::default_delete<derived> >&>::type' 
{aka 'std::unique_ptr<derived, std::default_delete<derived> >'} to non-scalar type 'test' requested

and from clang-7.0.0 is

candidate constructor not viable: no known conversion from 'unique_ptr<derived, default_delete<derived>>' 
to 'unique_ptr<base, default_delete<base>>' for 1st argument

Live code is available here.


Solution

  • A std::unique_ptr<base> is not the same type as a std::unique_ptr<derived>. When you do

    test t(std::move(pd));
    

    You call std::unique_ptr<base>'s conversion constructor to convert pd into a std::unique_ptr<base>. This is fine as you are allowed a single user defined conversion.

    In

    test t = std::move(pd);
    

    You are doing copy initialization so so you need to convert pd into a test. That requires 2 user defined conversions though and you can't do that. You first have to convert pd to a std::unique_ptr<base> and then you need to convert it to a test. It's not very intuitive but when you have

    type name = something;
    

    whatever something is needs to be only a single user defined conversion from the source type. In your case that means you need

    test t = test{std::move(pd)};
    

    which only uses a single implicit user defined like the first case does.


    Lets remove the std::unique_ptr and look at in a general case. Since std::unique_ptr<base> is not the same type as a std::unique_ptr<derived> we essentially have

    struct bar {};
    struct foo
    { 
        foo(bar) {} 
    };
    
    struct test
    {
        test(foo){}
    };
    
    int main()
    {
        test t = bar{};
    }
    

    and we get the same error because we need to go from bar -> foo -> test and that has one user defined conversion too many.