了解复制初始化和隐式转换

时间:2018-10-31 14:29:50

标签: c++ c++17 implicit-conversion unique-ptr

我无法理解为什么以下复制初始化无法编译:

#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
}

可以将unique_ptr<derived>移到unique_ptr<base>中,那么为什么第二条语句起作用而最后一条语句却不起作用?执行复制初始化时是否不考虑非显式构造函数?

gcc-8.2.0中的错误是:

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

从clang-7.0.0开始是

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

实时代码可用here

3 个答案:

答案 0 :(得分:20)

std::unique_ptr<base>std::unique_ptr<derived>的类型不同。当你做

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

您调用std::unique_ptr<base>的转换构造函数将pd转换为std::unique_ptr<base>。可以,因为您可以进行单个用户定义的转换。

test t = std::move(pd);

您正在执行副本初始化,因此需要将pd转换为test。但是,这需要2个用户定义的转换,而您不能这样做。您首先必须将pd转换为std::unique_ptr<base>,然后再将其转换为test。这不是很直观,但是当您拥有

type name = something;

无论something是什么,都只是源类型的单个用户定义的转换。就您而言,这意味着您需要

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

与第一种情况一样,它仅使用一个隐式用户定义。


让我们删除std::unique_ptr并进行一般检查。由于std::unique_ptr<base>std::unique_ptr<derived>的类型不同,因此我们基本上拥有

struct bar {};
struct foo
{ 
    foo(bar) {} 
};

struct test
{
    test(foo){}
};

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

we get the same error,因为我们需要从bar -> foo -> test开始,而用户定义的转换次数太多。

答案 1 :(得分:5)

[dcl.init] ¶17中描述了初始化程序的语义。直接初始化与复制初始化的选择将我们带入了两种不同的项目符号之一:

  

如果目标类型是(可能是cv限定的)类类型:

     
      
  • [...]

  •   
  • 否则,如果初始化是直接初始化,或者如果复制是初始化,则源的cv不合格版本   type是与该类相同的类,或者是该类的派生类   目的地,考虑构造函数。适用的构造函数   枚举([over.match.ctor]),然后通过选择最佳的   重载分辨率。如此选择的构造函数称为   使用初始化器表达式初始化对象或   expression-list作为其参数。如果没有构造函数适用,或者   重载解析不明确,初始化格式错误。

  •   
  • 否则(即,对于其余的复制初始化情况),可以从源转换的用户定义的转换序列   类型转换为目标类型或(使用转换功能时)   对其派生类的定义,如   [over.match.copy],最好的是通过重载选择的   解析度。如果转换无法完成或模棱两可,则   初始化格式错误。选定的函数用   初始化表达式作为其参数;如果函数是一个   构造函数,该调用是cv不合格版本的prvalue   其结果对象由初始化的目标类型   构造函数。该调用用于直接初始化,具体取决于   根据上述规则,作为目标的对象   复制初始化。

  •   

在直接初始化的情况下,我们输入第一个引用的项目符号。如此处所详述,构造函数被直接考虑并枚举。因此,所需的隐式转换序列仅是将unique_ptr<derived>转换为unique_ptr<base>作为构造函数参数。

在复制初始化的情况下,我们不再直接考虑构造函数,而是尝试查看哪种隐式转换序列是可能的。唯一可用的是unique_ptr<derived>unique_ptr<base>test。由于隐式转换序列只能包含一个用户定义的转换,因此不允许这样做。因此,初始化格式不正确。

可以说,使用直接初始化的“绕过”排序是一种隐式转换。

答案 2 :(得分:4)

请确保编译器仅考虑一次隐式转换。在第一种情况下,仅需要从std::unique_ptr<derived>&&std::unique_ptr<base>&&的转换,在第二种情况下,基本指针也需要转换为test(默认情况下,移动构造函数才能工作)。 因此,例如,将派生的指针转换为基数:std::unique_ptr<base> bd = std::move(pd),然后进行移动分配也可以。