Skip to content
C Codeloom
C++

C++ Move Semantics and Rvalue References

How move semantics work in modern C++: rvalue references, std::move, perfect forwarding, and the rules that decide when your objects are copied versus moved.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • What lvalues and rvalues actually are
  • How rvalue references enable move semantics
  • What std::move and std::forward really do
  • When the compiler skips moves (elision)
  • Common move-semantics pitfalls

Prerequisites

  • Basic familiarity with the language

Before C++11, returning a large std::vector by value sometimes meant copying every element. Compilers could elide the copy in many cases, but the language could not express the idea of stealing resources from a temporary. Move semantics fixed that. It is one of the most important language features added in the last fifteen years, and one of the most misunderstood.

Lvalues and rvalues

Every C++ expression has a value category. The two you need to know are lvalues (have a name and a stable address) and rvalues (temporaries about to disappear).

int x = 5;       // x is an lvalue
int y = x + 1;   // x + 1 is an rvalue
foo();           // the return value is an rvalue
std::string s = std::string("hi"); // the temporary on the right is an rvalue

You can take the address of an lvalue. You cannot take the address of an rvalue. The compiler uses this distinction to pick between copy and move operations.

Rvalue references

An rvalue reference, written T&&, binds to rvalues. It is the language hook that lets you write a function specialized for temporaries.

void take(const std::string& s); // binds to lvalues and rvalues, no theft
void take(std::string&& s);      // binds only to rvalues, can steal

std::string a = "hello";
take(a);             // calls the const ref overload
take(std::string()); // calls the rvalue ref overload

Inside the rvalue overload, s is itself an lvalue (it has a name). So if you want to actually move from it, you write std::move(s).

The mental model

A move is just a copy that is allowed to mutate the source. The destination takes ownership of the source’s heap allocation (or other resource), and the source is left in a valid-but-unspecified state.

Copy:  src [1,2,3] ----alloc + memcpy----> dst [1,2,3]
      src still owns its buffer

Move:  src [1,2,3] ----swap pointers-----> dst [1,2,3]
      src now [ ] (empty, valid)
Copy vs move for a vector

That is why std::move is fast and copy is not. For a std::vector<int> of a million elements, a copy allocates and writes a million ints. A move just swaps three pointers.

std::move

std::move does not actually move anything. It is a cast: static_cast<T&&>(x). It turns an lvalue into an rvalue so that move constructors and move assignments are selected.

std::vector<int> v = {1, 2, 3};
std::vector<int> w = std::move(v); // selects vector's move constructor
// v is now valid but unspecified, typically empty

After moving from an object, you can still destroy it or assign a new value to it. You should not assume anything else about its contents.

Writing move constructors

If you write a class that owns a resource (a buffer, a file handle), define both the move constructor and the move assignment operator. They steal the resource and null the source.

class Buffer {
    char* data;
    size_t size;
public:
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
};

Mark them noexcept. Containers like std::vector will use the move constructor during reallocation only if it is noexcept; otherwise they fall back to copy for exception safety.

Perfect forwarding

In generic code, you often want to pass arguments through to another function preserving their value category. The pattern uses a forwarding reference (also called universal reference) and std::forward.

template <typename T>
void wrapper(T&& arg) {
    inner(std::forward<T>(arg));
}

T&& here is special: deduced from an lvalue, T is U& and T&& collapses to U&. Deduced from an rvalue, T is U and T&& stays U&&. std::forward<T> casts back to whatever the caller had.

The result: wrapper(x) forwards as an lvalue; wrapper(std::string("hi")) forwards as an rvalue.

Copy elision

The compiler sometimes elides moves and copies entirely. Returning a local by value is the most common case (named return value optimization). Mandatory copy elision since C++17 means many of these elisions are guaranteed.

std::vector<int> make() {
    std::vector<int> v(1000);
    return v; // no move, no copy
}

Do not write return std::move(v) here. It actually disables the elision and forces an explicit move.

Common pitfalls

Using a moved-from object as if nothing happened. After std::move(s), s is in an unspecified state. Reading it is legal but the contents are undefined.

Marking move operations non-noexcept. STL containers will silently fall back to copying. Performance regresses without any warning.

Returning by const value. const T cannot be moved from, so a const return value forces a copy even when the caller would have benefited from a move.

Confusing rvalue references with forwarding references. T&& on a deduced template parameter is a forwarding reference. T&& on a concrete type (like std::string&&) is just an rvalue reference.

Practical tips

Define move operations on resource-owning classes. Use the rule of five: if you need a destructor, copy constructor, copy assignment, move constructor, or move assignment, you probably need all five (or default them explicitly). For value types built only from other movable types, let the compiler synthesize everything.

When passing by value to a function that will store the argument, pass by value and move inside: it works equally well for lvalues (copy) and rvalues (move) without two overloads.

Wrap-up

Move semantics turn ownership transfer from a copy into a pointer swap. The mechanics rest on rvalue references, std::move as a cast, and std::forward for generic forwarding. Get those three concepts straight and the rest follows. The payoff is fast code that still reads like value semantics, without the cost of copies.