显式移动构造函数

10

尝试编译以下代码:

struct Foo
{
    explicit Foo ( void ) { }
    explicit Foo ( Foo&& rhs ) { }
};

Foo bar ( void )
{
    return Foo();
}

遇到以下错误:

call to implicitly-deleted copy constructor of 'Foo'

很明显,复制构造函数被隐式删除了。

问题1:为什么编译器需要 Foo 的复制构造函数?我希望 bar 的返回值可以用移动构造函数从右值 Foo() 构造出来。

然后我将移动构造函数重新声明为隐式的,一切都成功编译了。

问题2:为什么当我将移动构造函数重新声明为隐式时,编译器不再需要复制构造函数?

问题3:在复制和移动构造函数的上下文中,“explicit”关键字意味着什么,因为它明显与常规构造函数的上下文不同?


@MarkGarcia 谢谢,但即使修复后所有问题仍然是实际存在的。 - Kolyunya
2
有趣。我从未想过“显式”会适用于左值/右值引用。 - Mark Garcia
@MarkGarcia 仍然需要在 Foo&&Foo 之间进行隐式转换。 - juanchopanza
请注意,该代码在(未标记的)C++17中完全正常。此外,即使复制和移动构造函数被删除/无法访问等,bar的定义也是可以的。 C++17使RVO成为强制性,并使用不同的规则进行应用。 - Daniel Langr
3个回答

10

这是因为返回一个值被认为是一种隐式转换

引用C++11标准:

6.6.3 返回语句

2 [...]

带有非void类型表达式的返回语句只能在返回值的函数中使用; 表达式的值将返回给函数的调用者。 表达式的值被隐式转换为其所在函数的返回类型。 返回语句可能涉及构造和复制或移动临时对象(12.2). [...]

从返回表达式到持有返回值的临时对象的转换是隐式的。因此,就像这样会导致错误的一样

Foo f = Foo();   // Copy initialization, which means implicit conversion

如果你的示例代码存在问题,那么它也可能会引发类似的问题。


问题1:为什么编译器需要Foo的复制构造函数?我期望bar的返回值由rvalue Foo()通过move-ctor构造。
这是因为Foo(Foo&&)不是可行的重载,因为上述原因。规则规定,每当移动构造函数无法使用时,编译器就会考虑复制构造函数,在您的情况下,由于存在用户定义的移动构造函数,复制构造函数被隐式删除。
问题2:为什么重新声明移动构造函数为implicit后编译器不再需要复制构造函数?
这是因为现在可以使用移动构造函数。因此,编译器可以立即使用它,甚至不考虑复制构造函数的存在。
问题3:在复制和移动构造函数的语境中,explicit关键字的含义与常规构造函数的语境明显不同,它意味着什么?
在我看来,这没有意义,只会导致问题,就像您的情况一样。

6
bar 的返回类型为 Foo。由于不存在复制构造函数,也不能使用显式移动构造函数,因为仍然需要在 Foo&&Foo 之间进行隐式转换。从这个意义上说,explicit Foo(Foo&&) 与任何其他 explicit 转换构造函数没有区别。如果将其它类型转换成 Foo,同样会出现这个问题。以下是一个用 int 类型类比的例子:
struct Foo
{
    explicit Foo(int) {}
};

Foo bar ( void )
{
    return 42; // error: could not convert '42' from 'int' to 'Foo'
}

Q1: 因为没有其他可用的选择。

Q2: 因为它使用移动构造函数将Foo&&隐式转换为Foo

Q3: 它与普通转换构造函数的含义相同。


什么是“移动-拷贝”构造函数? - Angew is no longer proud of SO
@Angew 只需写Foo(Foo&&); - juanchopanza
@Angew 我确定我在其他地方看到过这个术语。基本上,FooBar(Foo&&, Bar&&);也是一个移动构造函数。但我意识到这可能会让人感到困惑,所以我会进行编辑。 - juanchopanza
我认为FooBar(Foo&&, Bar&&)不是移动构造函数。至少根据C++11[class.copy]§3的规定,“如果类X的第一个参数类型为X&&const X&&volatile X&&const volatile X&&,并且没有其他参数或所有其他参数都有默认参数,则类X的非模板构造函数是移动构造函数。”无论如何,+1。 - Angew is no longer proud of SO
@Angew 你说得对。在这一切正式化之前,我可能早就学会了那个术语。 - juanchopanza

2
这与C ++中重载决议的工作方式有关。
重载决议的第一步是形成候选函数集。第二步是将候选函数集缩小到可行函数集。第三步是选择唯一的最佳可行函数,如果存在的话。如果最佳可行函数被删除,则程序不合法。
由于移动构造函数被声明为explicit,因此它不是将Foo()隐式转换为函数返回类型的候选函数。唯一的候选函数是Foo::Foo(const Foo&),即隐式声明的复制构造函数。尽管它被声明为已删除,但它仍然是一个候选函数。它通过重载决议被选中为唯一的可行函数;然后您会因尝试调用已删除的函数而得到错误。
如果您没有将移动构造函数声明为explicit,则移动构造函数和隐式声明的复制构造函数都是候选函数。在这种情况下,这两个函数也都是可行的。但是,移动构造函数赢得了重载决议,因为rvalue更喜欢绑定到rvalue引用而不是const lvalue引用。因此,移动构造函数是最佳可行函数。在这种情况下,即使复制构造函数被删除,也没有关系,因为它在重载决议中失败了。
简而言之:
答案1:因为移动构造函数不是转换的候选函数
答案2:因为移动构造函数是一个候选函数,并赢得了重载决议。
答案3:不,它意思相同。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接