【问题标题】:Why does std::move prevent RVO (return value optimization)?为什么 std::move 会阻止 RVO(返回值优化)?
【发布时间】:2013-10-16 12:56:29
【问题描述】:

在许多情况下,当从函数返回局部变量时,RVO(返回值优化)会起作用。但是,我认为显式使用 std::move 至少会在 RVO 不发生时强制移动,但仍然应用 RVO如果可能。然而,似乎并非如此。

#include "iostream"

class HeavyWeight
{
public:
    HeavyWeight()
    {
        std::cout << "ctor" << std::endl;
    }

    HeavyWeight(const HeavyWeight& other)
    {
        std::cout << "copy" << std::endl;
    }

    HeavyWeight(HeavyWeight&& other)
    {
        std::cout << "move" << std::endl;
    }
};

HeavyWeight MakeHeavy()
{
    HeavyWeight heavy;
    return heavy;
}

int main()
{
    auto heavy = MakeHeavy();
    return 0;
}

我用 VC++11 和 GCC 4.71 测试了这段代码,调试和发布 (-O2) 配置。复制 ctor 永远不会被调用。 move ctor 仅在调试配置中由 VC++11 调用。实际上,特别是这些编译器似乎一切都很好,但据我所知,RVO 是可选的。

但是,如果我明确使用move:

HeavyWeight MakeHeavy()
{
    HeavyWeight heavy;
    return std::move(heavy);
}

总是调用 move ctor。所以试图让它“安全”会让事情变得更糟。

我的问题是:

  • 为什么std::move 会阻止 RVO?
  • 什么时候最好“抱最好的希望”并依赖 RVO,什么时候应该明确使用std::move?或者,换句话说,如果不应用 RVO,我怎样才能让编译器优化完成它的工作并仍然强制移动?

【问题讨论】:

  • 为什么现在人们还在谈论“希望最好”?他们使用什么样的编译器支持 C++11 但不能正确 RVO?
  • 复制省略(RVO 背后的机制)仅在某些严格的条件下才被允许。写std::move 会阻止满足这些条件。
  • @KerrekSB 而 std::move 阻止的这些条件是......?
  • @Troy:你并不孤单。
  • @R.MartinhoFernandes:问题案例是允许行为更改的情况,即省略复制/移动构造函数调用。由于定义的测试用例必须包含副作用,因此您仅限于依赖复制省略和规则播放的优化。

标签: c++ c++11 move-semantics return-value-optimization


【解决方案1】:

标准(版本 N3690)第 12.8 节第 31 节中可以找到允许复制和移动省略的情况:

当满足某些条件时,允许实现省略类对象的复制/移动构造,即使为复制/移动操作选择的构造函数和/或对象的析构函数有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标简单地视为引用同一对象的两种不同方式,并且该对象的销毁发生在两个对象本应被删除的较晚时间。没有优化就被破坏了。这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的(可以组合起来消除多个副本):

  • 在具有类返回类型的函数中的 return 语句中,当表达式是具有相同 cv 非限定类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时函数返回类型,直接在函数返回值中构造自动对象即可省略复制/移动操作
  • [...]
  • 当一个没有绑定到引用 (12.2) 的临时类对象将被复制/移动到具有相同 cv-unqualified 类型的类对象时,可以通过直接构造临时对象来省略复制/移动操作进入省略的复制/移动的目标
  • [...]

(我省略的两种情况是指抛出和捕获异常对象的情况,我认为这对优化不太重要。)

因此,如果表达式是局部变量的名称,则只有在返回语句中才会出现复制省略。如果您写std::move(var),则它不再是变量的名称。因此,如果它应该符合标准,编译器就不能忽略这个动作。

Stephan T. Lavavej 在Going Native 2013 (Alternative source) 谈到了这个问题,并在此处准确解释了您的情况以及为什么要避免使用std::move()。从 38:04 分钟开始观看。基本上,当返回返回类型的局部变量时,它通常被视为右值,因此默认启用移动。

【讨论】:

  • 这是我所希望的答案。我对这种情况不满意,但我理解得更好。
  • 我们可能应该修复这样的问题,以便可以省略 return std::move。如果我们可以告诉 C++,一个函数的引用返回值保证与该函数的一个特定引用输入值相同,那么可能会出现一些有趣的结果。 (从重要的表达式中省略,将临时输入参数的生命周期延长到函数而不返回临时参数,对于两个:恕我直言,第二个更重要)。
  • 是的,我不明白为什么不能忽略这一点。只是更难做到,让编译器编写者有更多时间?
  • @Adrian 我的意思是允许它不会“给他们更少的时间”。保证会。如果只是允许,他们就不必实施。
  • @TomášRůžička,你的意思是不允许吗?
【解决方案2】:

如果不应用 RVO,我怎样才能让编译器优化完成它的工作并仍然强制移动?

像这样:

HeavyWeight MakeHeavy()
{
    HeavyWeight heavy;
    return heavy;
}

将返回转换为移动是强制性的。

【讨论】:

  • 那么在最坏的情况下保证返回本地是一个动作,而 RVO 在最好的情况下适用?
  • 我想我需要为 return std::move 做一些 grep'ping ;)
  • 这实际上是一个有用的警告。
  • @cdoubleplusgood 差不多。在最坏的情况下,保证直接返回本地非参数move。像return condition?heavy1:heavy2; 这样无害的东西可能会阻止隐含的move,而if (condition) return heavy1; return heavy2; 则不会。它有点脆弱。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-15
  • 2014-02-16
  • 1970-01-01
  • 2012-12-12
相关资源
最近更新 更多