为什么使用std::move会阻止返回值优化(RVO)?

68

在很多情况下,当从函数中返回一个本地对象时,会触发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)配置下进行测试。拷贝构造函数从未被调用过。移动构造函数只被VC++11在调试模式下调用。实际上,对于这些编译器而言一切似乎都正常,但据我所知,返回值优化是可选的。

然而,如果我显式地使用move

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

移动构造函数总是被调用。所以试图使其“安全”只会使情况变得更糟。

我的问题是:

  • 为什么std::move会阻止RVO?
  • 何时最好“抱有最好的愿望”并依赖于RVO,何时应该显式使用std::move?换句话说,如何让编译器优化发挥作用并在不应用RVO时强制执行移动操作?

6
为什么人们这些天还在谈论“抱最好的希望”?他们使用的是什么编译器,它支持C++11但无法正确执行RVO? - R. Martinho Fernandes
3
@KerrekSB,std::move 防止的这些条件是什么? - cdoubleplusgood
1
我一直认为通过进行显式的std::move操作来做到彻底,但事实上我一直在自我破坏!;) - goji
3
你不是孤单的。 - cdoubleplusgood
2
@R.MartinhoFernandes:问题的情况是允许行为更改的情况,即省略复制/移动构造函数调用。由于测试用例必须包含副作用,因此您只能使用依赖于复制省略并遵守规则的优化。 - Kerrek SB
显示剩余7条评论
2个回答

51
在标准(版本N3690)的第12.8条款31节中,可以找到允许复制和移动省略的情况:
当满足某些条件时,实现可以省略类对象的复制/移动构造,即使为复制/移动操作选择的构造函数和/或对象的析构函数具有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标视为引用同一对象的两种不同方式,并且该对象的销毁发生在两个对象无需优化地销毁的时间之后。这种复制/移动操作的省略称为复制省略,并且在以下情况下允许使用它(可以组合以消除多个副本):
- 在函数返回类型为类类型的return语句中,当表达式是非易失自动对象(而不是函数或catch-clause参数)的名称,并且具有与函数返回类型相同的去cv限定类型时,可以通过直接将自动对象构造到函数的返回值中来省略复制/移动操作。
  • [...]
  • 当一个未绑定到引用的临时类对象将被复制/移动到具有相同cv-非限定类型的类对象时,可以通过直接在省略的复制/移动目标中构造临时对象来省略复制/移动操作。
  • [...]

我忽略的两种情况涉及抛出和捕获异常对象的情况,我认为这对于优化不太重要。

因此,在返回语句中,只有本地变量的名称作为表达式才能发生复制省略。如果您编写std::move(var),那么它不再是变量的名称。因此,如果符合标准,则编译器无法省略移动。

Stephan T. Lavavej在Going Native 2013 (替代来源)上讲解了这个问题,并解释了为什么要避免在此处使用std::move()。从38:04开始观看。基本上,当返回与返回类型相同的局部变量时,通常会将其视为rvalue,因此默认启用移动操作。

6
我们可能需要修复程序,以便可以省略return std::move。如果我们能告诉C++函数的引用返回值保证与该函数的特定输入引用值相同,那么就能实现一些有趣的结果。(例如从非平凡表达式中省略、在不返回临时变量的情况下延长函数的临时输入参数的生命周期:这两个例子中后者更加重要,依我之见)。 - Yakk - Adam Nevraumont
2
是的,我不明白为什么不能省略这个。这只是更难做,让编译器编写者有更多的时间吗? - Adrian
@Adrian 我的意思是允许它并不会“给他们更少的时间”。保证才会。如果只是允许,他们不必实现它。 - Tomáš Růžička
@TomášRůžička,你的意思是不允许吗? - Adrian
@Adrien 当标准规定“你不能进行这种优化”时,实现者不必做这项工作。但如果他们说“你可以但不必”,实现者就不必做额外的工作……如果他们不想的话……因此,不允许优化并不会给他们节省任何时间。如果他们说“你必须进行这种优化”,那就是另一回事了。 - Tomáš Růžička
显示剩余2条评论

18

如果RVO未应用,我该如何让编译器优化发挥作用并仍然强制执行移动操作?

像这样:

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

将回报转化为移动是必须的。


1
那么返回本地变量保证是最坏情况下的移动,而在最好情况下适用于RVO? - cdoubleplusgood
2
我想我需要对 return std::move 进行一些 grep 操作 ;) - goji
7
那其实是一个有用的警告。 - MSalters
11
几乎正确。直接返回本地非参数将保证在最坏情况下进行“移动”。即使是类似于“return condition?heavy1:heavy2;”这样的无害语句也可能阻止隐式“移动”,而“if (condition) return heavy1; return heavy2;”则不会。它有点脆弱。 - Yakk - Adam Nevraumont

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