Skip to content
Laviyx Learn

Try

C++ course menu

C++ Move Semantics: rvalues, std::move and the Rule of Five

Understand C++ move semantics: lvalues and rvalues, rvalue references (&&), what std::move really does, move constructors and move assignment, noexcept moves, the Rule of Three, Rule of Five and Rule of Zero, and copy elision.

Advanced12 min readUpdated
On this page

Copying a large object, such as a vector with a million elements, means allocating new memory and copying every element. But often the original is about to be thrown away anyway: a temporary result, or a local variable being returned. Move semantics (C++11) lets C++ steal the resources of such an object instead of copying them, which is often dramatically faster.

lvalues and rvalues#

Roughly:

  • An lvalue has a name and an address; it persists beyond the expression (a variable such as name).
  • An rvalue is a temporary that is about to disappear (std::string("hi"), a + b, a function's return value).

It is safe to steal from an rvalue, because nobody can use it afterwards. An rvalue reference, written T&&, binds only to rvalues, and that is how a class detects "this argument may be moved from".

Watching copies and moves#

This class prints which special member function runs:

C++
#include <iostream>
#include <string>
#include <utility>

class Buffer {
public:
    explicit Buffer(std::string name) : name_(std::move(name)) {}

    Buffer(const Buffer& other) : name_(other.name_) {
        std::cout << "copy " << name_ << '\n';
    }
    Buffer(Buffer&& other) noexcept : name_(std::move(other.name_)) {
        std::cout << "move " << name_ << '\n';
    }

private:
    std::string name_;
};

int main() {
    Buffer a("A");
    Buffer b = a;              // a is an lvalue: copy
    Buffer c = std::move(a);   // std::move makes it an rvalue: move
    Buffer d = Buffer("D");    // a temporary: no copy or move at all (elision)
}
Output
copy A
move A

The last line prints nothing: since C++17, initializing from a temporary of the same type constructs the object directly in place (guaranteed copy elision).

What std::move does (and doesn't)#

std::move does not move anything. It is just a cast that says "treat this as an rvalue; I am done with it". The actual moving happens in the move constructor or move assignment operator that the cast makes the compiler choose.

After being moved from, an object is in a valid but unspecified state. You may assign it a new value or destroy it, but do not rely on its contents:

C++
#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::vector<std::string> names;
    std::string name = "Asha Rao";

    names.push_back(name);              // copies: name is still needed
    names.push_back(std::move(name));   // moves: name's text is taken

    std::cout << names[0] << " | " << names[1] << '\n';
    name = "Ravi";                      // fine: give it a new value
    std::cout << name << '\n';
}
Output
Asha Rao | Asha Rao
Ravi

Writing move operations#

For a class that owns a raw resource, the move constructor takes the other object's pointer and leaves the source empty, so its destructor does not free the stolen memory:

C++
#include <algorithm>
#include <iostream>
#include <utility>

class IntArray {
public:
    explicit IntArray(std::size_t n) : size_(n), data_(new int[n]{}) {}
    ~IntArray() { delete[] data_; }

    // copy: duplicate the resource
    IntArray(const IntArray& other) : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    IntArray& operator=(const IntArray& other) {
        if (this != &other) {
            IntArray temp(other);   // copy first, then swap: safe if new throws
            swap(temp);
        }
        return *this;
    }

    // move: steal the resource
    IntArray(IntArray&& other) noexcept
        : size_(std::exchange(other.size_, 0)), data_(std::exchange(other.data_, nullptr)) {}
    IntArray& operator=(IntArray&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            size_ = std::exchange(other.size_, 0);
            data_ = std::exchange(other.data_, nullptr);
        }
        return *this;
    }

    void swap(IntArray& other) noexcept {
        std::swap(size_, other.size_);
        std::swap(data_, other.data_);
    }

    std::size_t size() const { return size_; }
    int& operator[](std::size_t i) { return data_[i]; }

private:
    std::size_t size_;
    int* data_;
};

int main() {
    IntArray a(3);
    a[0] = 42;

    IntArray b = a;              // copy
    IntArray c = std::move(a);   // move: a is now empty

    std::cout << b[0] << ' ' << c[0] << ' ' << a.size() << '\n';
}
Output
42 42 0

std::exchange(x, new_value) sets x to new_value and returns its old value, which is exactly what stealing needs.

Mark move operations noexcept. Containers such as std::vector only move elements during reallocation if the move constructor cannot throw; otherwise they copy, to keep their strong exception guarantee.

The Rule of Three, Five and Zero#

  • Rule of Three: if a class needs a custom destructor, copy constructor or copy assignment, it almost certainly needs all three (because it manages a resource).
  • Rule of Five: in modern C++, add the move constructor and move assignment too.
  • Rule of Zero: best of all, design classes so they need none of the five. Hold resources in members that already manage themselves (std::vector, std::string, std::unique_ptr), and the compiler-generated versions do the right thing, including moves.
C++
#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Playlist {                       // Rule of Zero: no special members written
public:
    explicit Playlist(std::string name) : name_(std::move(name)) {}
    void add(std::string song) { songs_.push_back(std::move(song)); }
    std::size_t size() const { return songs_.size(); }

private:
    std::string name_;
    std::vector<std::string> songs_;
};

int main() {
    Playlist p("Focus");
    p.add("Track 1");
    p.add("Track 2");

    Playlist q = std::move(p);   // generated move constructor moves both members
    std::cout << q.size() << '\n';
}
Output
2

Writing the five special members by hand is mainly for low-level resource-owning classes, like the IntArray above. Everyday classes should follow the Rule of Zero.

Passing and returning in modern C++#

  • Return local objects by value: return result;. The compiler elides the copy or moves automatically. Writing return std::move(result); can actually prevent elision.
  • Sink parameters: when a function stores its argument (a constructor saving a name), take it by value and std::move it into place, as in Playlist(std::string name) : name_(std::move(name)). Callers passing a temporary then pay for one move instead of a copy.

Common mistakes#

Practice#

Practice exercise

Create three local strings, "alpha", "beta" and "gamma", and push_back them into a std::vector<std::string>: the first by copy and the other two with std::move. Print the vector, then count how many of the three local strings are now empty.

Show solution
C++
#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::string a = "alpha";
    std::string b = "beta";
    std::string c = "gamma";

    std::vector<std::string> v;
    v.push_back(a);
    v.push_back(std::move(b));
    v.push_back(std::move(c));

    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';

    int empty = 0;
    for (const std::string* s : {&a, &b, &c}) {
        if (s->empty()) ++empty;
    }
    std::cout << empty << " of the locals are empty\n";
}
Output
alpha beta gamma
2 of the locals are empty

Moved-from standard strings are left empty in practice by GCC, Clang and MSVC, but the standard only promises a "valid but unspecified" state, so real code should not depend on it.

Key takeaway#

Finished this lesson?