23

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.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
linuxfever
  • 3,763
  • 2
  • 19
  • 43

3 Answers3

20

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.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
5

The semantics of initializers are described in [dcl.init] ΒΆ17. The choice of direct initialization vs copy initialization takes us into one of two different bullets:

If the destination type is a (possibly cv-qualified) class type:

  • [...]

  • Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one is chosen through overload resolution. The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.

  • Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in [over.match.copy], and the best one is chosen through overload resolution. If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call is a prvalue of the cv-unqualified version of the destination type whose result object is initialized by the constructor. The call is used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization.

In the direct initialization case, we enter the first quoted bullet. As detailed there, constructors are considered and enumerated directly. The implicit conversion sequence that is required is therefore only to convert unique_ptr<derived> to a unique_ptr<base> as a constructor argument.

In the copy initialization case, we are not directly considering constructors anymore, but rather trying to see which implicit conversion sequence is possible. The only one available is unique_ptr<derived> to a unique_ptr<base> to a test. Since an implicit conversion sequence can contain only one user defined conversion, this is not allowed. As such the initialization is ill-formed.

One could say that using direct initialization sort of "bypasses" one implicit conversion.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
4

Pretty sure that only single implicit conversion is allowed to be considered by the compiler. In first case only conversion from std::unique_ptr<derived>&& to std::unique_ptr<base>&& is required, in the second case the base pointer would also need to be converted to test (for default move constructor to work). So for example converting the derived pointer to base: std::unique_ptr<base> bd = std::move(pd) and then move assigning it would work as well.

paler123
  • 976
  • 6
  • 18