【问题标题】:C++11: Move/Copy construction ambiguity?C++11:移动/复制构造歧义?
【发布时间】:2012-02-05 20:37:34
【问题描述】:

在 C++11 中,我们可以定义复制和移动构造函数,但都允许在同一个类上?如果是这样,你如何消除它们的用法?例如:

Foo MoveAFoo() {
  Foo f;
  return f;
}

以上是副本吗?一个动作?我怎么知道?

【问题讨论】:

  • 是的,复制和移动构造函数都允许在同一个类上。三法则变成了五法则。
  • @BenVoigt 看起来正在更新相应的维基百科文章;)

标签: c++ c++11


【解决方案1】:

通常都不是因为RVO

如果无法执行该优化,那么这将是一个移动,因为返回的对象超出了范围(并且将在之后被销毁)。如果它不能移动,那么它将被复制。如果无法复制,则无法编译。

移动构造函数的全部意义在于,当一个即将被销毁的对象要复制时,通常不需要复制整个副本,资源可以移动 从垂死的对象改为正在创建的对象。

您可以根据被移动/复制的对象将要发生的事情来判断何时调用复制或移动构造函数。它是否即将超出范围并被破坏?如果是这样,将调用移动构造函数。如果没有,复制构造函数。

当然,这意味着您可以在同一个类中同时拥有移动构造函数和复制构造函数。你也可以有一个复制赋值运算符和一个移动赋值运算符

更新:可能不清楚何时调用移动构造函数/赋值运算符与普通复制构造函数/赋值运算符。如果我理解正确,如果使用 xvalue(eXpiring 值)初始化对象,则会调用移动构造函数。标准的§3.10.1 说

一个xvalue(一个“eXpiring”值)也指一个对象,通常在附近 其生命周期的结束(以便可以移动其资源,因为 例子)。 xvalue 是某些类型的表达式的结果 涉及右值引用(8.3.2)。 [ 例子:调用的结果 返回类型为右值引用的函数是 xvalue。 -结尾 例子]

标准第 5 节的开头说:

[ 注意:表达式是一个 xvalue 如果它是:

  • 调用结果 函数,无论是隐式还是显式,其返回类型都是 对象类型的右值引用,
  • 对右值引用的强制转换 对象类型,
  • 一个类成员访问表达式指定一个 对象所在的非引用类型的非静态数据成员 表达式是一个 xvalue,或
  • .* 指向成员的表达式 其中第一个操作数是 xvalue,第二个操作数是 指向数据成员的指针。

一般来说,这条规则的效果是 命名的右值引用被视为左值和未命名的右值 对对象的引用被视为 xvalues;右值引用 无论是否命名,函数都被视为左值。 ——尾注]


举个例子,如果可以做NRVO,是这样的:

void MoveAFoo(Foo* f) {
    new (f) Foo;
}

Foo myfoo; // pretend this isn't default constructed
MoveAFoo(&myfoo);

如果不能做 NRVO 但Foo 是可移动的,那么你的例子有点像这样:

void MoveAFoo(Foo* fparam) {
    Foo f;

    new (fparam) Foo(std::move(f));
}

Foo f; // pretend this isn't being default constructed
MoveAFoo(&f);

如果不能移动但可以复制,那么就是这样

void MoveAFoo(Foo* fparam) {
    Foo f;

    new (fparam) Foo((Foo&)f);
}

Foo f; // pretend this isn't default constructed
MoveAFoo(&f);

【讨论】:

  • 您描述如果编译器检测到源左值将在此后不久被丢弃,则选择移动构造函数。这是在标准中定义的(可能在 Xeo 对 C++ 标准的引用中)?发现这种情况似乎有点困难。你能再强调一点吗? Xeo 对标准的引用听起来像是总是首先选择移动构造函数,只有在它不匹配或不存在时才会选择复制构造函数。
  • @Robert:你忽略了我引用的第一句话——“当满足复制省略的标准时”。
  • @RobertDailey 至于编译器检测何时使用移动构造函数,它会在使用 x 值(eXpiring 值)初始化对象时这样做
  • @Xeo 没有被解雇,只是不明白。出于所有意图和目的,您对 C++ 标准的引用是用不同的语言表达的。
  • @SethCarnegie 我假设 C++11 标准也定义了“x 值”要求?还是这个实现定义的?
【解决方案2】:

要备份@Seth,以下是标准中的相关段落:

§12.8 [class.copy] p32

当满足或将满足删除复制操作的条件时,除了源对象是函数参数这一事实之外,并且要复制的对象由左值指定,重载决议选择复制的构造函数首先执行,就好像对象是由右值指定的一样。如果重载决议失败,或者如果所选构造函数的第一个参数的类型不是对对象类型的右值引用(可能是 cv 限定的),则再次执行重载决议,将对象视为左值。 [ 注意: 无论是否会发生复制省略,都必须执行此两阶段重载解决方案。它确定如果不执行省略则要调用的构造函数,并且即使调用被省略,所选构造函数也必须是可访问的。 ——尾注 ]

【讨论】:

  • 我已经读了 10 遍了,但我还是不太明白它在说什么。不幸的是,这就是为什么我不能真正依赖 C++ 标准来理解这一点。标准的语言太“神秘”了。
  • @Robert:那部分首先描述了复制省略适用的情况。其中之一是函数返回值。然后这部分说,像X& obj 这样的对象将首先被尝试移动(如X&&),如果失败,它将尝试复制为X&,无论是否实际发生复制省略。
  • @Xeo 我没有看到任何关于返回值的内容。您具体翻译的是哪一部分?
  • @Robert:此处不包括允许复制省略的部分。这是一个很长的列表,所以我不愿意在这里复制并粘贴它......但是您可以在 12.8/31 下的 N3337 paper(基本上是标准)中查找自己。
【解决方案3】:

“消歧”只是你的老朋友,重载决议:

Foo y;

Foo x(y);            // copy
Foo x(std::move(y)); // move

第一个示例中的表达式yFoo 类型的左值,它绑定到Foo const &(如果你有这样的构造函数,还绑定到Foo &);第二个例子中的表达式std::move(y)的类型是Foo &&,所以它会绑定到Foo &&(还有Foo const &没有前者)。

在您的示例中,MoveAFoo() 的结果是 Foo 类型的临时结果,因此如果可用,它将绑定到 Foo &&-constructor,否则绑定到 const-copy 构造函数。

最后,在返回Foo(按值)的函数中,如果xFoo 类型的局部变量,则语句return x; 等价于return std::move(x);——这是一个特殊的新规则使移动语义的使用更容易。

【讨论】:

  • static_assert( not std::is_same<decltype(MoveAFoo()), Foo&&>::value, "the type of MoveAFoo() is not Foo&&");
  • 这并不完全正确。 std::move(y) 确实是 Foo&&,但 MoveAFoo()Foo。我相信它们有两种不同的价值类别——一种是 xvalue,另一种是 prvalue。虽然它们都是右值,但说MoveAFoo() 返回Foo&& 是不正确的。您通过声明y 的类型是Foo& 而犯了类似的错误 - 它不是。它是Foo 类型的左值。
  • 啊,抱歉,y 成为 Foo& 也是如此 :)
【解决方案4】:
Foo MoveAFoo() {
  Foo f;
  return f;
}

这是函数MoveAFoo 的定义,它返回Foo 类型的对象。在其主体中,本地 Foo f; 在超出其范围时被创建和销毁。

在这段代码中:

Foo x = MoveAFoo();

对象Foo f;MoveAFoo函数内部创建,直接赋值给x,即不调用拷贝构造函数。

但在这段代码中:

Foo x;
x = MoveAFoo();

对象Foo f;MoveAFoo函数内部创建,然后f的副本被创建并存储到x中,原始f被破坏。

【讨论】:

  • 在最后一个例子中,原来的 Foo x;创建 x。然后在当语句 x = MoveAFoo();执行时,值 Foo() 被移动到 x 中(或存在移动省略),这导致 f 的值与 x 的值交换。最后一个操作是原始 x 值被破坏的情况。这里没有抄袭!
猜你喜欢
  • 2018-01-23
  • 2012-05-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-22
  • 2014-01-13
  • 2013-11-05
  • 1970-01-01
相关资源
最近更新 更多