在RVO不能完成的情况下,我们应该写'std::move'吗?

Should we write `std::move` in the cases when RVO can not be done?

已知 std::move 不应应用于函数 return 值,因为它可以防止 RVO(return 值优化)。我对这个问题很感兴趣,如果我们肯定知道 RVO 不会发生,我们应该怎么做。

这就是C++14标准所说的[12.8/32]

When the criteria for elision of a copy/move operation are met, but not for an exception-declaration, and the object to be copied is designated by an lvalue, or when the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. — end note ]

这里是书中的解释Effective Modern C++

The part of the Standard blessing the RVO goes on to say that if the conditions for the RVO are met, but compilers choose not to perform copy elision, the object being returned must be treated as an rvalue. In effect, the Standard requires that when the RVO is permitted, either copy elision takes place or std::move is implicitly applied to local objects being returned

据我了解,当return对象一开始不能被省略时,应该被视为rvalue。在这些示例中,我们可以看到当我们传递大于 5 的参数时,对象被移动,否则它被复制。这是否意味着我们应该在知道不会发生 RVO 的情况下显式写入 std::move

#include <iostream>
#include <string>


struct Test
{
    Test() {}

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

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

Test foo(int param)
{
    Test test1;
    Test test2;
    return param > 5 ? std::move(test1) : test2;
}

int main()
{
    Test res = foo(2);
}

这个程序的输出是Test(const Test&)

您的示例中发生的事情与 RVO 无关,而是与三元 operator ? 相关。如果您使用 if 语句重写您的示例代码,程序的行为将是预期的。将 foo 定义更改为:

Test foo(int param)
  {
  Test test1;
  Test test2;
  if (param > 5)
    return std::move(test2);
  else
    return test1;
  }

会输出Test(Test&&).


如果你写 (param>5)?std::move(test1):test2 会发生什么:

  1. 三元运算符结果推导为 prvalue [expr.cond]/5
  2. 然后test2通过lvalue-to-rvalue conversion which causes copy-initialization as required in [expr.cond]/6
  3. 然后省略 return 值的移动构造 [class.copy]/31.3

所以在您的示例代码中,发生了移动省略,但是在形成三元运算符的结果所需的复制初始化之后。

事实上,在您的示例中发生了复制省略。如果您使用参数 -fno-elide-constructors 明确禁止 RVO/NRVO,那么它可能会打印(我使用 Apple clang 版本 13.0.0 和 Homebrew GCC 11.2.0_2)

Test(const Test&)
Test(const Test&&)
Test(const Test&&)

调用第一个复制构造函数以计算表达式 param > 5 ? std::move(test1) : test2。由于复制省略不可用,移动构造函数将至少被调用一次。所以我认为在 return 语句上添加 std::move 总是多余的。