有没有一个简单的移动构造示例不会被省略?

7
我正在尝试学习移动语义,以便向我的学生介绍。我一直在使用高度简化的类似于向量或字符串的类来管理内存,并输出消息以展示它们的活动。我正在尝试开发一组简单的示例来向学生展示。
在gcc 4.7和clang中,用于RVO和其他地方的构造省略会积极消除复制和移动构造函数,因此,虽然我可以轻松看到移动赋值的工作原理,但我只有在关闭gcc 4.7中的构造省略(-fno-elide-constructors)时才能看到移动构造函数的工作原理。
一个显式的复制构造语句。
MyString newString(oldString);

即使启用了省略,它也会调用复制构造函数。但是像这样的代码:

MyString newString(oldString1 + oldString2); 

由于省略,不会调用移动构造函数。

任何明确使用std::move的内容都不会形成简单示例,因为解释std::move必须在后面进行。

所以我的问题是:是否有一个简单的代码示例,即使复制/移动构造函数被省略,也会调用移动构造函数?


我不确定在没有使用 std::move 的情况下是否可以完成... - Mooing Duck
1
“任何明确使用std :: move的内容都不会成为一个简单的例子,因为解释std :: move必须在后面进行。” 为什么? std :: move显式移动对象的方法。移动支持主要涉及显式移动,因为大多数隐式移动可以通过省略来捕获。通过将move隐藏起来,您会对从中学习的任何人造成不利影响。 - Nicol Bolas
@NicolBolas: 因为在理论上存在隐式移动。想象一下 OP 请求中级优化水平:oldString1+oldString2 触发了一个复制(这里没有优化),但是该复制被移动到 newString 中(优化)。实际上,这很难触发,因为编译器选择“复制省略”优化而不是“移动复制”优化(有充分的理由)。 - André Caron
我将不得不解释std::move,但如果我能在不必首先介绍std::move(其解释有点棘手)的情况下演示移动构造,那将会很有帮助。 - user1628444
@NicolBolas - 不好意思,我应该在上面加上前缀来表明我的评论是回复你的。 - user1628444
你可以使用 static_cast 替代 move,至少在初始说明中。 - Nevin
2个回答

7
简单的例子是将参数传递给返回的函数。标准明确禁止在这种情况下省略移动(尽管他们可能会...):
std::vector<int> multiply( std::vector<int> input, int value ) {
   for (auto& i : input )
      i *= value;
   return input;
}

此外,您可以明确要求移动构造,以获得更简单但略微人工的示例:
T a;
T b( std::move(a) );

嗯...另一种不涉及std::move的方法(它在技术上可以省略,但大多数编译器可能不会):

std::vector<int> create( bool large ) {
   std::vector<int> v1 = f();
   std::vector<int> v2 = v1;       // modify both v1 and v2 somehow
   v2.resize( v2.size()/2 );
   if ( large ) {
      return v1;
   } else {
      return v2;
   }
}

虽然优化器可以通过重写代码来省略它:

std::vector<int> create( bool large ) {
   if ( large ) {
      std::vector<int> v1 = f();
      std::vector<int> v2 = v1;       // modify both v1 and v2 somehow
      v2.resize( v2.size()/2 );
      return v1;
   } else {
      std::vector<int> v1 = f();
      std::vector<int> v2 = v1;       // modify both v1 and v2 somehow
      v2.resize( v2.size()/2 );
      return v2;
   }
}

我非常怀疑编译器实际上会这样做。请注意,在每个代码路径中,在创建v1v2之前已知要返回的对象,因此优化器可以在重写后定位正确的对象到返回位置。

可以省略复制/移动的情况在12.8/31中描述。如果您成功编写了不属于这些类别的代码,并且该类型具有移动构造函数,则将调用移动构造函数。


这个限制是因为否则,实现可能会试图省略参数和返回值吗? - André Caron
1
@AndréCaron:复制省略通过重用相同的内存来处理源对象和目标对象。例如,在 T a = f(); 中,编译器可以将 a 对象定位在与 f() 返回值相同的位置。问题在于调用约定通常确定参数和返回对象的位置,因此编译器无法强制它们处于同一位置。有关复制省略的更多信息,请参见这里这里 - David Rodríguez - dribeas
嗯,也许我有点困惑。现在我再次看你的例子,我无法想象为什么 input 参数不能是返回值本身(除了实际编译器实现之外)。标准为什么禁止这样做? - André Caron
@AndréCaron:因为标准规定如此。因为至少必须有一个副本,因为该函数按值接收参数并按值返回值。因此,将调用复制构造函数或移动构造函数中的一个 - Nicol Bolas
好的,所以如果编译器恰好内联了函数(如此代码片段中所假设的),那么可能可以绕过这个限制。但在一般情况下(例如编译器没有为特定调用站点内联函数),这种优化是不可能的。我明白了! - André Caron
显示剩余7条评论

1

嗯,让我们看看:

  • MyString newString(oldString) 一份副本。这里没有什么可以省略的;我们最终确实会得到两个对象。

  • MyString newString(oldString1 + oldString2);从临时变量中复制,因此可以省略复制并直接在原地构建连接。

这是一个非常糟糕的无法省略移动构造的例子:

MyString boo()
{
    MyString s("Hello");
    return std::move(s);   // move-construction from the local "s", never elided
}

显式转换使s不符合RVO的条件,因此返回值将从s进行移动构造。


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