Move Semantics

C++ · Lesson 25 of 32 7 min read Updated

Copying a std::vector with a million items means allocating new memory and copying a million values. Often the original is never used again, for example when a function returns a vector or you put a temporary string into a container. In that case, copying is wasted work: it would be much faster to simply hand over the memory the original already owns. That hand-over is called a move, and it is one of the biggest performance features added in C++11.

In this lesson you will learn the difference between lvalues and rvalues, what std::move really does, how to give your own class a move constructor, and the common mistakes to avoid.

Copy versus move

C++
#include <print>
#include <utility>
#include <vector>

int main() {
    std::vector<int> readings(1'000'000, 7);

    std::vector<int> backup = readings;              // copy: a million ints duplicated
    std::vector<int> archive = std::move(readings);  // move: the memory is handed over

    std::println("backup: {} items", backup.size());
    std::println("archive: {} items", archive.size());
}
Output
backup: 1000000 items
archive: 1000000 items

Both new vectors have a million items, but the work was very different. The copy allocated new memory and copied every value. The move only transferred three internal pointers from readings to archive, which takes the same tiny time whether the vector holds ten items or ten million. After the move, readings is in a valid but unspecified state: you may assign a new value to it or destroy it, but you should not rely on what it contains.

lvalues, rvalues and rvalue references

To decide between copying and moving, C++ looks at the value category of an expression. A simplified view:

  • An lvalue has a name and a lasting identity, such as a variable name. You might use it again, so C++ copies from it.
  • An rvalue is a temporary, such as the result of name + " Singh" or a value returned from a function. Nobody can use it afterwards, so it is safe to move from it.

An rvalue reference, written T&&, binds only to rvalues. By overloading on const T& and T&&, a function can copy from lvalues and move from rvalues. The standard library does exactly this, for example in std::vector::push_back.

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

void store(const std::string& s) { std::println("copying \"{}\"", s); }
void store(std::string&& s)      { std::println("moving \"{}\"", s); }

int main() {
    std::string name = "Kabir";
    store(name);                  // lvalue
    store(name + " Singh");       // temporary: rvalue
    store(std::move(name));       // lvalue turned into an rvalue
}
Output
copying "Kabir"
moving "Kabir Singh"
moving "Kabir"

std::move (header <utility>) does not move anything by itself. It is only a cast that says "treat this lvalue as an rvalue, I promise not to rely on its value afterwards". The actual moving happens in the function or constructor that receives it.

Writing a move constructor

When a class is copied from an lvalue, its copy constructor runs; when it is created from an rvalue, its move constructor runs. This class prints a message in each, so you can see which one is used.

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

class Document {
public:
    explicit Document(std::string title) : title_(std::move(title)) {}

    Document(const Document& other) : title_(other.title_) {
        std::println("copy {}", title_);
    }

    Document(Document&& other) noexcept : title_(std::move(other.title_)) {
        std::println("move {}", title_);
    }

    Document& operator=(const Document&) = default;
    Document& operator=(Document&&) noexcept = default;

private:
    std::string title_;
};

int main() {
    Document a("Fees");
    Document b = a;                 // a is an lvalue: copy
    Document c = std::move(a);      // cast to rvalue: move
    Document d = Document("Notes"); // C++17: built directly in d
}
Output
copy Fees
move Fees

The move constructor takes Document&& and moves each member, here by calling std::move on the string, which steals its characters. For d, nothing is printed: since C++17, initialising an object from a temporary of the same type is guaranteed to build it directly in place, with no copy and no move. This is called copy elision. Something similar happens when a function returns a local variable, as in Document make() { Document local("Report"); return local; }: the compiler is allowed to build local directly in the caller's object (named return value optimisation), and if it does not, the return statement moves the local, never copies it.

Mark move operations noexcept. Containers such as std::vector move their elements when they grow only if the move constructor promises not to throw; otherwise they copy, to keep their strong error-safety guarantee.

Moves in everyday code

You rarely write move constructors yourself: if your class holds standard types, the compiler generates correct move operations (the rule of zero). What you do write every day is code that takes advantage of moves:

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

class Contact {
public:
    Contact(std::string name, std::string phone)        // take by value...
        : name_(std::move(name)), phone_(std::move(phone)) {}   // ...then move in

    const std::string& name() const { return name_; }

private:
    std::string name_;
    std::string phone_;
};

int main() {
    std::vector<Contact> phonebook;

    std::string who = "Ananya Rao";
    phonebook.push_back(Contact(who, "98765 43210"));     // who is copied, still usable
    phonebook.emplace_back("Rohan Das", "91234 56789");   // built in place

    std::string temp = "Isha Khan";
    phonebook.emplace_back(std::move(temp), "99887 76655"); // temp is not needed again

    for (const auto& c : phonebook) {
        std::println("{}", c.name());
    }
    std::println("who is still {}", who);
}
Output
Ananya Rao
Rohan Das
Isha Khan
who is still Ananya Rao

The constructor takes its strings by value and moves them into the members. A caller with an lvalue pays one copy; a caller with a temporary pays only moves. emplace_back passes its arguments to the constructor and builds the element directly inside the vector, avoiding even the temporary Contact.

Common mistakes

  • Using an object after moving from it. After std::move(name), do not read name until you assign it a new value.
  • Writing return std::move(local);. It blocks copy elision and can make code slower. Write return local;.
  • Moving from const objects. std::move on a const value produces a const rvalue, which cannot bind to T&&, so it silently copies.
  • Forgetting noexcept on move constructors. std::vector will then copy elements when it grows.
  • Adding std::move everywhere. Move only when you are finished with an object; for small types such as int a move is just a copy.

Key points

  • A move transfers the resources of an object that is no longer needed instead of copying them.
  • lvalues have names and are copied from; rvalues are temporaries and can be moved from. T&& binds to rvalues.
  • std::move is only a cast to an rvalue; the receiving constructor or function does the move.
  • Moved-from objects are valid but unspecified: assign or destroy them only.
  • Mark move constructors noexcept, never return std::move(local);, and rely on copy elision for returned values.
  • Take "sink" parameters by value and std::move them into members; use emplace_back to build in place.

Exercise

Write a function std::vector<std::string> load_names() that creates a local vector with three names from Jaipur and returns it. In main, store the result, then move the whole vector into a second vector backup with std::move. Print the size of backup, then assign a fresh list of two names to the original vector and print its size to show that a moved-from vector can be reused.

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

std::vector<std::string> load_names() {
    std::vector<std::string> names = {"Aarav", "Priya", "Meera"};
    return names;                         // no std::move needed
}

int main() {
    std::vector<std::string> names = load_names();
    std::vector<std::string> backup = std::move(names);
    std::println("backup holds {} names", backup.size());

    names = {"Farhan", "Isha"};           // reuse the moved-from vector
    std::println("names holds {} names again", names.size());
}
Output
backup holds 3 names
names holds 2 names again

After the move, names is only assigned a new value before being used again, which is always safe. The function returns its local vector without std::move, so the compiler can elide the move entirely.

Quick Quiz

1. What does std::move(x) do by itself?

2. Which overload does store(name + "!") call when both store(const std::string&) and store(std::string&&) exist?

3. What can you safely do with an object after moving from it?

4. Why should move constructors be noexcept?

Learn C++ with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call