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:
- The compiler checks whether copy elision (NRVO) can be performed.
- 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.
- 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.