C++ Copy Elision with Rvalue Return Expressions

In C++, copy constructor elision optimizes performance by eliminating redundant copy and move operations when returning values from functions. The interaction between copy elision and rvalue return expressions depends on whether the return expression is a prvalue (pure rvalue) or an xvalue (an expiring value resulting from an explicit cast like std::move). While returning prvalue expressions guarantees elision under modern C++ standards, returning xvalues actively suppresses Named Return Value Optimization (NRVO), shifting execution from zero-overhead construction to move semantics.

Guaranteed Elision with Prvalues

Starting in C++17, copy elision is mandatory when a function returns a prvalue expression of the same class type as the return type (excluding cv-qualifiers). For example:

Widget make_widget() {
    return Widget(); // Prvalue
}

In this context, the prvalue is evaluated directly into the storage of the caller's target object. The copy and move constructors are not merely omitted; they are not invoked at all, and they do not even need to be defined or accessible. Here, the rvalue expression achieves the most optimal form of elision, known as Return Value Optimization (RVO).

Named Return Value Optimization (NRVO) vs. Xvalues

When returning an existing, named local variable (an lvalue), the compiler may apply NRVO to construct the local object directly within the caller’s return slot:

Widget make_widget() {
    Widget w;
    // ... configure w ...
    return w; // Eligible for NRVO
}

NRVO is an optional optimization, though almost all modern compilers implement it when optimization flags are enabled.

A common misconception occurs when developers attempt to "optimize" this pattern by returning an explicit rvalue reference via std::move:

Widget make_widget() {
    Widget w;
    // ... configure w ...
    return std::move(w); // Pessimization: returns an xvalue
}

Applying std::move casts the expression to an xvalue. Because the C++ standard requires the operand of an elided return statement to be the unqualified name of a non-volatile automatic object, an xvalue expression disqualifies the function from NRVO. The compiler is explicitly forbidden from eliding the operation in this scenario, forcing it to construct the returned object via the move constructor instead.

Two-Stage Overload Resolution and Implicit Move

Writing an explicit rvalue cast in a return statement is unnecessary because the language provides automatic fallback semantics. When returning a local variable by value, C++ mandates a two-stage overload resolution:

  1. The compiler checks whether copy elision (NRVO) can be performed.
  2. If elision is not performed (or is disabled), the compiler automatically treats the local variable as an rvalue. It looks for a matching move constructor first.
  3. If no viable move constructor exists, the compiler falls back to the copy constructor.

Because the compiler already treats the returned variable as an rvalue when elision fails or is skipped, using std::move provides no benefit. It only serves to eliminate the possibility of NRVO, turning a potential zero-cost elision into a mandatory move operation.