【问题标题】:Understanding copy-initialisation and implicit conversions了解复制初始化和隐式转换
【发布时间】:2018-10-31 14:29:50
【问题描述】:

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

#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&lt;derived&gt; 可以移动到unique_ptr&lt;base&gt;,那么为什么第二个语句有效,而最后一个无效?执行复制初始化时是否不考虑非显式构造函数?

来自 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

【问题讨论】:

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


    【解决方案1】:

    std::unique_ptr&lt;base&gt;std::unique_ptr&lt;derived&gt; 的类型不同。当你这样做时

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

    您调用std::unique_ptr&lt;base&gt; 的转换构造函数将pd 转换为std::unique_ptr&lt;base&gt;。这很好,因为您可以进行一次用户定义的转换。

    test t = std::move(pd);
    

    您正在进行复制初始化,因此您需要将pd 转换为test。不过,这需要 2 次用户定义的转换,而您不能这样做。您首先必须将pd 转换为std::unique_ptr&lt;base&gt;,然后您需要将其转换为test。这不是很直观,但是当你有时

    type name = something;
    

    无论something 是什么,都只需要来自源类型的单个用户定义转换。在您的情况下,这意味着您需要

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

    它只使用像第一种情况那样定义的单个隐式用户。


    让我们删除std::unique_ptr 并查看一般情况。由于std::unique_ptr&lt;base&gt;std::unique_ptr&lt;derived&gt; 的类型不同,因此我们基本上拥有

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

    we get the same error,因为我们需要从bar -&gt; foo -&gt; test 开始,并且其中一个用户定义的转换太多。

    【讨论】:

      【解决方案2】:

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

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

      • [...]

      • 否则,如果初始化是直接初始化,或者如果是复制初始化,其中源的 cv 不合格版本 type 与该类的类相同或派生类 目的地,构造函数被考虑。适用的构造函数 被枚举([over.match.ctor]),并通过 重载决议。如此选择的构造函数被调用到 使用初始化表达式或初始化对象 表达式列表作为其参数。如果没有构造函数适用,或者 重载决议不明确,初始化格式不正确。

      • 否则(即,对于剩余的复制初始化情况),可以从源转换的用户定义的转换序列 类型为目标类型或(使用转换函数时) 其派生类的枚举如中所述 [over.match.copy],通过重载选择最好的 解析度。如果转换无法完成或不明确,则 初始化格式不正确。选择的函数被调用 初始化表达式作为其参数;如果函数是 构造函数,调用是 cv 非限定版本的纯右值 结果对象由 构造函数。该调用用于直接初始化,根据 上面的规则,作为目标的对象 复制初始化。

      在直接初始化的情况下,我们输入第一个引用的项目符号。正如那里所详述的,构造函数被直接考虑和枚举。因此,所需的隐式转换序列只是将unique_ptr&lt;derived&gt; 转换为unique_ptr&lt;base&gt; 作为构造函数参数。

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

      可以说使用直接初始化“绕过”了一次隐式转换。

      【讨论】:

        【解决方案3】:

        非常确定编译器只允许考虑单个隐式转换。在第一种情况下,只需要从std::unique_ptr&lt;derived&gt;&amp;&amp;std::unique_ptr&lt;base&gt;&amp;&amp; 的转换,在第二种情况下,基指针也需要转换为test(默认移动构造函数才能工作)。 因此,例如将派生指针转换为基指针:std::unique_ptr&lt;base&gt; bd = std::move(pd),然后移动分配它也可以。

        【讨论】:

          猜你喜欢
          • 2013-01-16
          • 1970-01-01
          • 2016-04-27
          • 1970-01-01
          • 1970-01-01
          • 2015-01-12
          • 2015-07-13
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多