Question
C++11 Return Statements, Rvalue References, and Move Semantics
Question
I am trying to understand C++11 rvalue references and move semantics. What is the difference between the following examples, and which ones avoid copying a std::vector<int>?
std::vector<int> return_vector()
{
std::vector<int> tmp{1, 2, 3, 4, 5};
return tmp;
}
std::vector<int>&& rval_ref = return_vector();
std::vector<int>&& return_vector()
{
std::vector<int> tmp{1, 2, 3, 4, 5};
return std::move(tmp);
}
std::vector<int>&& rval_ref = return_vector();
std::vector<int> return_vector()
{
std::vector<int> tmp{1, 2, 3, 4, 5};
return std::move(tmp);
}
std::vector<int>&& rval_ref = return_vector();
In particular, when should std::move be used in a return statement?
Short Answer
Returning a local object by value is normally both safe and efficient in C++11. The compiler may apply Named Return Value Optimization (NRVO), constructing the result directly in the caller. If NRVO is not applied, C++ can usually move the local object automatically.
Do not return an rvalue reference to a local variable: the local is destroyed when the function ends, leaving a dangling reference. Also, avoid return std::move(local); for a normal local return value because it can prevent NRVO.
Concept
A function return has two separate concerns:
- Ownership and lifetime: Is the returned object still alive after the function returns?
- Transfer cost: Is the result copied, moved, or constructed directly in its final location?
For an object created inside a function, returning by value gives the caller its own result object:
std::vector<int> make_numbers()
{
std::vector<int> numbers{1, 2, 3};
return numbers;
}
This is safe because numbers is not returned as a reference. The function returns a new std::vector<int> result.
NRVO
When returning a named local variable whose type matches the function return type, a compiler may use NRVO. Instead of creating numbers and later moving or copying it into the result, the compiler constructs numbers directly in the caller's destination.
Implicit move
If NRVO is not used, C++11 has a special rule for an eligible local object returned by value. In this case:
return numbers;
the compiler can treat numbers as movable and call the move constructor when one is available. std::vector has an efficient move constructor.
Why returning T&& is dangerous
An rvalue reference such as std::vector<int>&& is still a reference. It does not own an object and does not extend the lifetime of a local object returned from another function.
A local variable is destroyed at the closing brace of its function. Returning a reference to that local therefore creates a dangling reference, and using it produces undefined behavior.
Mental Model
Think of a local std::vector as a box stored in a function's temporary workspace.
- Returning by value means the function gives the caller a new box. NRVO may mean the box was built directly on the caller's desk. Otherwise, the function may efficiently hand over the box's contents using a move.
- Returning by reference means the function gives the caller directions to a box in its own workspace.
- Returning a reference to a local box is unsafe because the workspace is cleared as soon as the function finishes. The directions now point to something that no longer exists.
std::move is not a moving operation by itself. It is a label that says, “this object may be moved from.” The actual move happens only if an operation, such as a move constructor, uses that label.
Syntax and Examples
Returning a local object by value is the usual and recommended syntax:
#include <vector>
std::vector<int> make_numbers()
{
std::vector<int> numbers{1, 2, 3, 4, 5};
return numbers;
}
int main()
{
std::vector<int> values = make_numbers();
}
The return statement is simply return numbers;.
- The compiler may use NRVO, so no separate move or copy is needed.
- If NRVO is not performed,
numbersis eligible for an implicit move. - A
std::vectormove normally transfers its internal storage ownership rather than copying every integer.
Binding the result to an rvalue reference
This is valid, but is rarely the best choice:
std::vector<int>&& values = make_numbers();
The temporary returned by make_numbers() lives as long as the local reference variable values. However, the expression values is an lvalue because it has a name:
// `values` is an lvalue expression here.
std::vector<int> copy = values;
To explicitly move from that named reference, use std::move(values):
std::vector<int> moved = std::move(values);
Usually, just store the result by value instead:
std::vector<int> values = make_numbers();
Avoid this form for local return values
std::vector<int> make_numbers()
{
std::vector<int> numbers{1, 2, 3};
return std::move(numbers); // Usually unnecessary and can block NRVO
}
Use return numbers; instead.
Step by Step Execution
Consider this recommended version:
std::vector<int> make_numbers()
{
std::vector<int> tmp{1, 2, 3, 4, 5};
return tmp;
}
int main()
{
std::vector<int> result = make_numbers();
}
Step by step:
make_numbers()begins.- The local vector
tmpis initialized with five integers. - The function reaches
return tmp;. - The compiler may apply NRVO and construct
tmpdirectly asresultinmain. - If NRVO is not applied,
tmpis an eligible local object, so its move constructor can be selected instead of its copy constructor. tmpreaches the end of its lifetime. If it was moved from, it remains valid but has an unspecified value.resultowns the returned vector and can be used normally.
Now consider the unsafe version:
std::vector<int>&& broken()
{
std::vector<int> tmp{1, 2, 3};
return std::move(tmp);
}
tmpis created insidebroken().std::move(tmp)caststmpto an rvalue expression.- The function returns a reference to
tmp; it does not create a new vector for the caller. broken()ends, sotmpis destroyed.- The caller receives a reference to destroyed storage. Any use of it is undefined behavior.
Real World Use Cases
Returning objects by value is common in modern C++:
-
Factory functions: Create and return a configured object.
std::vector<std::string> load_lines(); -
Parsing: Return a parsed collection or result object.
std::vector<int> parse_ids(std::string_view text); -
Data transformations: Build a filtered or converted container.
std::vector<int> positive_values(const std::vector<int>& input); -
Error-aware APIs: Return an object containing either a value or error information, such as
std::optional<T>orstd::expected<T, E>where available.
These APIs are clear because the caller receives an object it owns. NRVO and move semantics make value returns practical even for containers and larger user-defined types.
Real Codebase Usage
In production C++, prefer straightforward value-returning functions:
std::vector<int> filter_even(const std::vector<int>& input)
{
std::vector<int> output;
for (int value : input) {
if (value % 2 == 0) {
output.push_back(value);
}
}
return output;
}
Developers normally rely on NRVO and implicit moves rather than writing std::move in the return statement.
When std::move in a return can be appropriate
It can be useful when returning a non-local object, such as a data member, and moving is explicitly intended:
class BufferBuilder {
public:
std::vector<int> finish()
{
return std::move(buffer_);
}
private:
std::vector<int> buffer_;
};
Here, buffer_ is not a local automatic variable eligible for the local-return implicit move rule. Moving communicates that finish() transfers the builder's stored buffer. Afterward, buffer_ is valid but its contents are unspecified.
Use this deliberately: callers should understand that calling finish() consumes the builder's accumulated buffer.
Common Mistakes
Returning a reference to a local variable
Broken code:
std::vector<int>&& make_numbers()
{
std::vector<int> tmp{1, 2, 3};
return std::move(tmp);
}
tmp is destroyed when the function returns. The reference dangles.
Avoid it: Return by value.
std::vector<int> make_numbers()
{
std::vector<int> tmp{1, 2, 3};
return tmp;
}
Adding std::move to every return
return std::move(tmp); // Usually worse for a local returned by value
For a named local, this can disable NRVO. It may still move instead of copy, but it can force an unnecessary move that NRVO would have eliminated.
Avoid it: Write return tmp; for a local object returned by value.
Assuming std::move moves immediately
std::move(tmp);
This only casts tmp to an rvalue expression. If the result is ignored, no move occurs.
Avoid it: Pass the result to an operation that can move, such as initialization:
std::vector<int> destination = std::move(tmp);
Treating a named rvalue reference as an rvalue
std::vector<int>&& ref = make_numbers();
std::vector<int> second = ref; // Copies: `ref` is a named lvalue expression.
Avoid it: Use std::move(ref) only when you truly want to move from it, or store the result as a normal value in the first place.
Relying on a moved-from vector's contents
After moving from a vector, it remains valid, but its size and elements are unspecified.
std::vector<int> b = std::move(a);
// Do not assume a is empty.
You may assign to, clear, or destroy a, but do not depend on its old contents.
Comparisons
| Return form | Safe when returning a local? | NRVO possible? | Typical result |
|---|---|---|---|
return tmp; from a T function | Yes | Yes | NRVO, or an implicit move if NRVO is not used |
return std::move(tmp); from a T function | Yes | Usually no | Move construction, but potentially less efficient than NRVO |
return tmp; from a T& or T&& function | No | No | Returns a dangling reference |
return std::move(tmp); from a T&& function | No | No | Returns a dangling reference |
Value return versus reference return
| Choice | Meaning | Good use case |
|---|---|---|
T | The caller receives its own object. | Creating a result inside the function. |
T& | The caller aliases an existing object. | Returning an object owned elsewhere that will outlive the call. |
const T& | The caller observes an existing object without modifying it through the reference. | Avoiding a copy when the owner clearly outlives the reference. |
T&& | The caller receives an rvalue reference to an existing object. | Specialized forwarding or consuming interfaces; not local variables. |
For a function that creates a new vector, T—that is, std::vector<int>—is the correct return type.
Cheat Sheet
// Recommended: safe and efficient
std::vector<int> make_vector()
{
std::vector<int> tmp{1, 2, 3};
return tmp;
}
- Return a local object by value.
- For
return local;, compilers can use NRVO. - If NRVO is not used, an eligible local is usually implicitly moved in C++11 and later.
- Do not write
return std::move(local);for an ordinary local returned by value; it can prevent NRVO. - Never return
T&orT&&referring to a local variable. std::move(x)is a cast; it does not itself perform a move.- A named
T&&variable is an lvalue expression. Usestd::move(name)to move from it. - A moved-from object is valid but has an unspecified value.
// Unsafe: dangling reference
std::vector<int>&& bad()
{
std::vector<int> local{1, 2, 3};
return std::move(local);
}
FAQ
Does return tmp; copy a vector in C++11?
Not necessarily. NRVO may eliminate the copy and move entirely. If NRVO is not used, C++11 can usually move an eligible local variable instead of copying it.
Should I use std::move when returning a local variable?
Usually no. Use return local;. Adding std::move can prevent NRVO, which is often better than a move.
Is return std::move(tmp); always wrong?
No. It is usually unnecessary for a local returned by value, but can be intentional when returning a member or another non-local object that you want to move from.
Why is returning std::vector<int>&& from a local unsafe?
The local vector is destroyed when the function returns. The rvalue reference refers to an object that no longer exists.
Does binding a function result to std::vector<int>&& avoid a copy?
It can bind directly to the returned temporary, but it is rarely useful. Prefer std::vector<int> value = make_vector();; this also benefits from copy elision or moves and gives a clear owning object.
Is a named rvalue reference an rvalue?
No. Although its type is T&&, a named expression is an lvalue. Use std::move(reference) if you intend to move from it.
Is moving a std::vector<int> the same as copying its integers?
Typically no. A vector move usually transfers ownership of internal storage rather than copying every element. Exact implementation details can vary, especially with custom allocators.
Mini Project
Description
Create a function that filters a list of integers and returns a newly built vector. This demonstrates the normal real-world pattern: build a local result, return it by value, and let NRVO or move semantics handle efficient transfer.
Goal
Implement a function that returns all even numbers from an input vector without returning references to local objects.
Requirements
Create a function named even_numbers that accepts const std::vector<int>&. Return a std::vector<int> by value. Add only values divisible by 2 to the result. In main, print the returned values. Do not use std::move in the return statement.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.