Using std::move() when returning a value from a function to avoid to copy

C++

C++ Problem Overview


Consider a type T supporting the default move semantics. Also consider the function below:

T f() {
   T t;
   return t;
}

T o = f();

In the old C++03, some non-optimal compilers might call the copy constructor twice, one for the "return object" and one for o.

In c++11, since t inside f() is an lvalue, those compilers might call the copy constructor one time as before, and then call the move constructor for o.

Is it correct to state that the only way to avoid the first "extra copy" is to move t when returning?

T f() {
   T t;
   return std::move(t);
}

C++ Solutions


Solution 1 - C++

No. Whenever a local variable in a return statement is eligible for copy elision, it binds to an rvalue re­fe­rence, and thus return t; is identical to return std::move(t); in your example with respect to which constructors are eligible.

Note however that return std::move(t); prevents the compiler from exercising copy elision, while return t; does not, and thus the latter is the preferred style. [Thanks to @Johannes for the cor­rect­ion.] If copy elision happens, the question of whether or not move construction is used becomes a moot point.

See 12.8(31, 32) in the standard.

Note also that if T has an accessible copy- but a deleted move-constructor, then return t; will not com­pile, because the move constructor must be considered first; you'd have to say something to the ef­fect of return static_cast<T&>(t); to make it work:

T f()
{
    T t;
    return t;                 // most likely elided entirely
    return std::move(t);      // uses T::T(T &&) if defined; error if deleted or inaccessible
    return static_cast<T&>(t) // uses T::T(T const &)
}

Solution 2 - C++

No. The best practice is directly return t;.

In case class T has move constructor not deleted, and notice t is a local variable that return t is eligible for copy elision, it move constructs the returned object just like return std::move(t); does. However return t; is still eligible to copy/move elision, so the construction may be omitted, while return std::move(t) always constructs the return value using move constructor.

In case move constructor in class T is deleted but copy constructor available, return std::move(t); will not compile, while return t; still compiles using copy constructor. Unlike @Kerrek mentioned, t is not bound to an rvalue reference. There's a two-stage overload resolution for return values that eligible for copy elision -- try move first, then copy, and both move and copy is possibly elided.

class T
{
public:
    T () = default;
    T (T&& t) = delete;
    T (const T& t) = default;
};

T foo()
{
    T t;
    return t;                   // OK: copied, possibly elided
    return std::move(t);        // error: move constructor deleted
    return static_cast<T&>(t);  // OK: copied, never elided
}

If the return expression is lvalue and not eligible for copy elision (most likely you are returning a non-local variable or lvalue expression) and you still would like to avoid copy, std::move will be useful. But keep in mind that the best practice is make copy elision possible to happen.

class T
{
 public:
    T () = default;
    T (T&& t) = default;
    T (const T& t) = default;
};
    
T bar(bool k)
{
    T a, b;
    return k ? a : b;            // lvalue expression, copied
    return std::move(k ? a : b); // moved
    if (k)
        return a;                // moved, and possibly elided
    else
        return b;                // moved, and possibly elided
}

12.8(32) in the standard describes the process.

> 12.8 [class.copy] > > 32 When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If overload resolution fails, 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 ]

Solution 3 - C++

Ok, I would like to drop a comment on this. This question (and the answer) made me believe that it is not necessary to specify std::move on the return statement. However I was just thought a different lesson while dealing with my code.

So, I have a function (it's actually a specialization) that takes a temporary and just returns it. (The general function template does other stuff, but the specialization does the identity operation).

template<>
struct CreateLeaf< A >
{
  typedef A Leaf_t;
  inline static
  Leaf_t make( A &&a) { 
    return a;
  }
};

Now, this version calls the copy constructor of A upon returning. If I change the return statement to

Leaf_t make( A &&a) { 
  return std::move(a);
}

Then the move constructor of A gets called and I can do some optimizations there.

It might not be 100% matching your question. But it is false to think that return std::move(..) is never necessary. I used to think so. Not any more ;-)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMartinView Question on Stackoverflow
Solution 1 - C++Kerrek SBView Answer on Stackoverflow
Solution 2 - C++TinroView Answer on Stackoverflow
Solution 3 - C++ritterView Answer on Stackoverflow