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

2

8

It is known that std::move should not be applied to the function return values because it can prevent RVO (return value optimization). I am interested in the question what should we do if we certainly know that RVO will not happen.

This is what the C++14 standard says [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 ]

Here is the explanation from the book 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

As I understand when return object can't be elided at first it should be regarded as rvalue. In these example we can see that when we pass argument greater than 5 object is moved otherwise it is copied. Does it mean that we should explicitly write std::move when we know that RVO will not happen?

#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);
}

The output of this program is Test(const Test&).

Confirmand answered 14/1, 2018 at 15:4 Comment(8)
"As I understand when return object can't be elided at first it should be regarded as rvalue" As long as the conditions for copy elision are met (or they would be met except the object is a function parameter.)Impressment
Are you also interested in C++17 answers?Tot
If using std::move does it move the local to the temporary on the right side of the equals sign and then further call either copy or move constructor?Windowshop
@Yakk I just want to understand what is going here. It is recommended not to write std::move on return values but in this example it would be copied it I not write it. It I change ? operator to if it would always moved...Confirmand
@Impressment object is not function parameter so do copy elision conditions are met? Why it is copied in that case?Confirmand
@ashot you have no move on the return value in your example. You have one in the return value expression. So, I do not know what you do not understand.Camiecamila
Shouldn't it be return std::move(param > 5 ? test1 : test2);? (since test1 and test2 are of the same type, the result of that ternary expression is a reference, which is not the case in your example)Irena
Generally no and unless you are performing a type conversion return std::move(...) is pointless.Rixdollar
C
10

What happen in your example is not linked to RVO, but to the ternary operator ?. If you rewrite your example code using an if statement, the behavior of the program will be the one expected. Change foo definition to:

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

will output Test(Test&&).


What happens if you write (param>5)?std::move(test1):test2 is:

  1. The ternary operator result is deduced to be a prvalue [expr.cond]/5
  2. Then test2 pass through lvalue-to-rvalue conversion which causes copy-initialization as required in [expr.cond]/6
  3. Then the move construction of the return value is elided [class.copy]/31.3

So in your example code, move elision occurs, nevertheless after the copy-initialization required to form the result of the ternary operator.

Clarinda answered 14/1, 2018 at 16:59 Comment(0)
B
0

In fact in your example the copy elision is happened. If you explicitly forbid RVO/NRVO with the argument -fno-elide-constructors, then it possibly print (I used Apple clang version 13.0.0 and Homebrew GCC 11.2.0_2)

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

The first copy constructor is called in order to evaluate expression param > 5 ? std::move(test1) : test2. And the move constructor will be called at least once since copy elision is unavailable. So I think add std::move on return statement is always superfluous.

Bibulous answered 22/1, 2022 at 16:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.