C++ use of std::move for return of rvalue function parameter?

Summary

The code snippet demonstrates a common misconception about C++ return value optimization (RVO) and copy elision. The function T foo(T&& t) { return std::move(t); } explicitly moves a function parameter. Since t is an rvalue reference parameter, it is constructed in the caller’s stack frame, making it ineligible for RVO. While preventing compiler optimizations like copy elision is not a concern here, the explicit std::move forces a move operation where the compiler might have otherwise performed RVO. However, in this specific context, the move is unnecessary because returning a local variable or parameter implicitly moves if it is an rvalue or an xvalue. The question arises whether such a move is safe or beneficial, and the answer is nuanced: it is safe but often stylistically discouraged unless the parameter is a prvalue or xvalue that the compiler cannot elide.

Root Cause

The root cause of the potential inefficiency or confusion is the mandatory move semantics applied to function parameters and local variables in C++11 and later. When a function returns a local variable or a parameter, the compiler is allowed to perform implicit move construction if the return value is not a candidate for copy elision (RVO/NRVO).

  • Function parameters passed by rvalue reference are constructed by the caller and are lvalues inside the function. Returning them requires explicit std::move to treat them as rvalues, otherwise the compiler will perform a copy (which may be optimized to a move if the parameter is a temporary).
  • RVO eligibility is strictly limited to variables or temporaries created within the function body that are returned directly. Function parameters, even rvalue references, are created outside the function and thus cannot benefit from RVO.
  • The explicit std::move in the example forces a move operation, but it does not block RVO because RVO is not applicable. However, it is redundant if the parameter is already an rvalue or xvalue, as the compiler would implicitly move.

Why This Happens in Real Systems

In real-world C++ code, developers often use std::move on return statements to ensure move semantics, especially when dealing with types that are expensive to copy. This pattern emerges because:

  • Implicit moves are only guaranteed for local variables and parameters in specific contexts (since C++11), but developers may not trust the compiler to perform them correctly.
  • Legacy code or pre-C++11 code might have relied on explicit copies, and modernizing it can lead to overuse of std::move.
  • Template metaprogramming or generic code can obscure the type of the return expression, making developers opt for explicit moves to avoid accidental copies.
  • Performance tuning in hot paths leads engineers to micro-optimize, sometimes adding std::move where it is unnecessary, not realizing that the compiler is already optimizing.

Real-World Impact

The impact of using std::move on function parameters in return statements is generally minimal but can have subtle effects:

  • No performance gain: Since RVO is impossible for function parameters, the explicit move does not improve performance over an implicit move. However, it ensures a move is performed, which is cheap for most types.
  • Potential code bloat: Overusing std::move can make code less readable and may confuse other developers about the ownership semantics.
  • Compilation overhead: Slight increase in template instantiation complexity, but negligible.
  • Safety concerns: If the moved-from object is used later (e.g., if t is passed by reference and reused), explicit moves can lead to bugs. However, in the example, t is an rvalue reference, so it is likely a temporary and should not be reused.
  • Strictly positive impact in some cases: For types without implicit move support (e.g., due to user-defined constructors), explicit std::move might be necessary.

Example or Code

#include 

struct T {
    T() = default;
    T(const T&) = default; // Copy constructor
    T(T&&) = default;      // Move constructor
};

// Example 1: Explicit move of rvalue parameter - often unnecessary
T foo(T&& t) {
    return std::move(t); // Forces a move; compiler might have moved implicitly
}

// Example 2: Implicit move - compiler will move if possible
T bar(T&& t) {
    return t; // Implicit move if t is an xvalue (rvalue reference)
}

// Example 3: RVO eligible - parameter not involved
T baz() {
    T local;
    return local; // NRVO may apply; explicit std::move(local) blocks it
}

In foo, the explicit std::move is safe but redundant because t is an rvalue reference (an xvalue), and the compiler would move it implicitly. In bar, returning t directly allows implicit move construction. In baz, std::move would block NRVO, so it is avoided.

How Senior Engineers Fix It

Senior engineers address this by leveraging compiler optimizations and adhering to best practices:

  • Avoid explicit std::move on return statements for local variables and parameters: Trust the compiler to perform implicit moves. Use return local; instead of return std::move(local);.
  • Use RVO-friendly code: Design functions to return objects directly, avoiding unnecessary moves. For function parameters, consider passing by value with std::move in the caller if possible.
  • Profile before optimizing: Measure performance with tools like perf or VTune to confirm if explicit moves are needed. Implicit moves are usually optimal.
  • Adopt C++17/20 guidelines: In modern C++, copy elision is mandatory for prvalues, so focus on that. For rvalue parameters, returning them directly is sufficient.
  • Code reviews: Enforce rules like “no std::move in return statements unless the type is not movable” to prevent over-optimization.
  • Education: Train teams on the rules of implicit move (C++11 [class.copy]/32) and RVO eligibility.

Why Juniors Miss It

Junior engineers often miss these nuances due to:

  • Overemphasis on std::move: Tutorials and blogs highlight std::move as a performance panacea without explaining its limitations, leading to cargo-cult programming.
  • Lack of deep understanding of C++ semantics: Many juniors are unfamiliar with the intricacies of copy elision, RVO, and implicit move rules, which are advanced topics.
  • Fear of copies: In performance-critical code, juniors might add std::move “just in case,” not realizing that the compiler can optimize moves implicitly.
  • Insufficient experience with profiling: Without measuring performance, they assume explicit moves are always better.
  • Code review gaps: Juniors might not have mentors to correct the overuse of std::move, perpetuating bad habits in team codebases.