【问题标题】:Is there a simple example of move construction that won't get elided?是否有一个不会被省略的移动构造的简单示例?
【发布时间】:2012-08-30 10:20:44
【问题描述】:

我正在努力学习足够好的移动语义,以便将其介绍给我的学生。我一直在使用高度简化的向量或类似字符串的类来管理内存,并且其成员输出消息以展示它们的活动。我正在尝试开发一组简单的示例来向学生展示。

gcc 4.7 中 RVO 和其他地方的构造省略和 clang 积极地消除了复制和移动构造,所以虽然我可以很容易地在工作中看到移动分配,但我唯一一次在工作中看到移动构造是如果我关闭构造省略在带有 -fno-elide-constructors 的 gcc 4.7 中。

显式复制构造语句

MyString newString(oldString);

即使启用了省略,也会调用复制构造函数。但是像

MyString newString(oldString1 + oldString2); 

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

任何显式使用 std::move 的东西都不会是一个简单的例子,因为解释 std::move 必须在后面。

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

【问题讨论】:

  • 我不确定没有std::move...
  • "任何显式使用 std::move 的东西都不会是一个简单的例子,因为解释 std::move 必须在后面。" 嗯,为什么? std::move明确移动事物的方式。移动支持主要是关于显式 移动,因为大多数隐式移动都可以通过省略来捕获。通过向他们隐藏move,您正在对任何从中学习的人造成伤害。
  • @NicolBolas:因为有隐含的动作,至少在理论上是这样。想想 OP 要求中间级别的优化:oldString1+oldString2 触发一个副本(这里没有优化),但副本被移动到 newString(优化)。实际上,这很难触发,因为编译器选择了“复制省略”优化而不是“移动复制”优化(有充分的理由)。
  • 我将不得不解释 std::move,但如果我能够演示移动构造而无需先介绍 std::move(其解释有点棘手),这将有所帮助。
  • @NicolBolas - 抱歉,我应该在上面的评论前面加上前缀,以表明我的评论是对你的回应。

标签: c++ c++11 move-semantics


【解决方案1】:

简单的例子是返回的函数的参数。该标准明确禁止在这种情况下省略移动(不是他们可以......):

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:复制省略通过为源对象和目标对象重用相同的内存来工作。例如,在T a = f(); 中,编译器可以将a 对象定位在与f() 返回值相同的位置。问题是调用约定通常确定参数和返回对象的位置,因此编译器不能强制它们在同一个位置。更多关于复制省略 herehere
  • 是的,参数和返回值可以在同一个地方(至少在某些情况下)。想想int f(int x){return x;} 和激进的内联。在您提供的示例中,您不能同时省略参数复制构造和返回值,因为您需要构造至少一个新对象来保存函数的结果。我的问题是为什么这是一个普遍的限制?
  • 嗯,也许我很困惑。现在我再次查看您的示例,我无法想象为什么 input 参数不能是返回值本身(除了实际的编译器实现)。为什么标准禁止它?
  • @AndréCaron:因为标准是这样说的。因为必须至少有 一个 副本,因为该函数按值获取参数并按值返回值。因此,必须调用复制构造函数或移动构造函数
【解决方案2】:

嗯,让我们看看:

  • 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 移动构造。

【讨论】:

    猜你喜欢
    • 2016-06-01
    • 2023-03-03
    • 1970-01-01
    • 1970-01-01
    • 2015-11-20
    • 1970-01-01
    • 1970-01-01
    • 2014-01-02
    • 1970-01-01
    相关资源
    最近更新 更多