我无法理解为什么以下复制初始化无法编译:
#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>
可以移动到 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
可在此处获取实时代码。
A
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{};
}
并且我们得到相同的错误,因为我们需要从
bar -> foo -> test
开始,并且一个用户定义的转换太多。
初始化器的语义在 [dcl.init] ¶17 中描述。直接初始化与复制初始化的选择将我们带入两种不同的项目之一:
如果目标类型是(可能是 cv 限定的)类类型:
[...]
否则,如果初始化是直接初始化,或者是复制初始化,其中源的 cv 不合格版本 type 是与该类相同的类,或者是该类的派生类 目的地,构造函数被考虑。适用的构造函数 被枚举([over.match.ctor]),并且通过选择最好的一个 过载决议。如此选择的构造函数被调用 使用初始化表达式或 表达式列表作为其参数。如果没有构造函数适用,或者 重载解析不明确,初始化格式错误。
否则(即,对于其余的复制初始化情况),可以从源进行转换的用户定义的转换序列 类型转换为目标类型或(当使用转换函数时) 其派生类的枚举如下所述 [over.match.copy],通过重载选择最好的一个 解决。如果转换无法完成或不明确,则 初始化格式不正确。所选择的函数被调用 初始化表达式作为其参数;如果函数是 构造函数,调用是 cv 未限定版本的纯右值 其结果对象由以下项初始化的目标类型 构造函数。该调用用于直接初始化,根据 上面的规则中,作为目的地的对象 复制初始化。
在直接初始化的情况下,我们输入第一个引用的项目符号。正如那里所详述的,构造函数被直接考虑和枚举。因此,所需的隐式转换序列只是将
unique_ptr<derived>
转换为 unique_ptr<base>
作为构造函数参数。
在复制初始化的情况下,我们不再直接考虑构造函数,而是尝试看看哪种隐式转换序列是可能的。唯一可用的是
unique_ptr<derived>
到 unique_ptr<base>
到 test
。由于隐式转换序列只能包含一个用户定义的转换,因此这是不允许的。因此初始化是不正确的。
可以说,使用直接初始化“绕过”了一种隐式转换。
非常确定编译器只允许考虑单个隐式转换。在第一种情况下,仅需要从
std::unique_ptr<derived>&&
到 std::unique_ptr<base>&&
的转换,在第二种情况下,基指针也需要转换为 test
(默认移动构造函数才能工作)。
因此,例如将派生指针转换为基数:std::unique_ptr<base> bd = std::move(pd)
,然后移动分配它也可以工作。