62

在许多情况下,当从函数返回一个局部变量时,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,我怎样才能让编译器优化完成它的工作并且仍然强制移动?
4

2 回答 2

45

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

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

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

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

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

Stephan T. Lavavej 在Going Native 2013 (替代来源)上谈到了这一点,并准确地解释了你的情况以及为什么要避免std::move()在这里。从 38:04 开始观看。基本上,当返回返回类型的局部变量时,它通常被视为右值,因此默认启用移动。

于 2013-10-09T12:28:41.920 回答
17

如果未应用 RVO,如何让编译器优化完成其工作并仍然强制执行移动?

像这样:

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

将回报转化为移动是强制性的。

于 2013-10-09T09:04:40.603 回答