Question

C++11 Return Statements, Rvalue References, and Move Semantics

cppc++11move-semanticsrvalue-referencec++-faq

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:

  1. Ownership and lifetime: Is the returned object still alive after the function returns?
  2. 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.

Take Quiz

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, numbers is eligible for an implicit move.
  • A std::vector move 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:

  1. make_numbers() begins.
  2. The local vector tmp is initialized with five integers.
  3. The function reaches return tmp;.
  4. The compiler may apply NRVO and construct tmp directly as result in main.
  5. If NRVO is not applied, tmp is an eligible local object, so its move constructor can be selected instead of its copy constructor.
  6. tmp reaches the end of its lifetime. If it was moved from, it remains valid but has an unspecified value.
  7. result owns 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);
}
  1. tmp is created inside broken().
  2. std::move(tmp) casts tmp to an rvalue expression.
  3. The function returns a reference to tmp; it does not create a new vector for the caller.
  4. broken() ends, so tmp is destroyed.
  5. 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> or std::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.

Take Quiz

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 formSafe when returning a local?NRVO possible?Typical result
return tmp; from a T functionYesYesNRVO, or an implicit move if NRVO is not used
return std::move(tmp); from a T functionYesUsually noMove construction, but potentially less efficient than NRVO
return tmp; from a T& or T&& functionNoNoReturns a dangling reference
return std::move(tmp); from a T&& functionNoNoReturns a dangling reference

Value return versus reference return

ChoiceMeaningGood use case
TThe 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.

Take Quiz

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& or T&& 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. Use std::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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more