Skip to content

Intermediate · Templates, smart pointers and move semantics · topic 29 of 42 · 35 min

Move semantics: steal instead of copy

Explanation

Moving transfers the insides (like a heap buffer) from one object to another instead of copying them, which makes passing and returning big objects cheap. Temporary values (rvalues) are moved automatically; std::move marks a named object as "you may move from me". After a move, the source is valid but its value is unspecified.

The problem: expensive copies

A std::vector<std::string> with a lakh names holds a big heap buffer. Copying it means allocating a new buffer and copying every string. But often the source is about to disappear anyway: a temporary result from a function, or a local variable you will never use again. Copying in those cases is pure waste. Moving instead just takes over the source's buffer pointer and leaves the source empty: a few pointer assignments, no matter how big the vector is.

Think of it like this: Copying is like photocopying every page of a file to give it to a colleague. Moving is like handing over the file itself: it is instant, and you no longer have it. You only move when you do not need the file any more.

lvalues and rvalues

An lvalue is an expression that names an object with an identity you can refer to again, like a variable name. An rvalue is a temporary value that is about to disappear, like the result of a + b or a function returning by value. C++ automatically moves from rvalues because nobody can use them afterwards. For lvalues it copies, because you might still use them, unless you say otherwise with std::move.

cpp
std::string makeName() { return "Asha Verma"; }

std::string a = "hello";
std::string b = a;               // a is an lvalue: COPY
std::string c = makeName();      // temporary: MOVE (or no copy at all, see below)
std::string d = std::move(a);    // std::move: MOVE; a is now valid but unspecified (usually empty)
std::move does not move anything by itself. It is a cast that says "treat this as an rvalue", which lets the move constructor be chosen.

Rvalue references (T&&)

An rvalue reference, written T&&, is a reference that binds only to rvalues. Classes use it to write a move constructor and a move assignment operator: overloads that are chosen when the source is an rvalue, and that steal its resources.

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

    Buffer(Buffer&& other) noexcept                 // move constructor
        : size_(other.size_), data_(other.data_) {
        other.size_ = 0;
        other.data_ = nullptr;                      // leave the source safe to destroy
    }
    Buffer& operator=(Buffer&& other) noexcept {    // move assignment
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
    Buffer(const Buffer&) = delete;                 // copying not supported in this example
    Buffer& operator=(const Buffer&) = delete;
private:
    std::size_t size_;
    int* data_;
};
The move just copies two values and nulls out the source. delete[] nullptr is safe, so the source can be destroyed normally.

Why noexcept matters

When std::vector grows, it must transfer its elements to the new buffer. If the element's move constructor is marked noexcept, vector moves them (fast). If not, vector copies them instead, to keep its strong exception guarantee. So always mark move operations noexcept when they cannot throw (they usually cannot).

Return values: no std::move needed

cpp
std::vector<int> loadMarks() {
    std::vector<int> result(100000, 0);
    return result;               // do NOT write return std::move(result);
}
Returning a local by name is automatically moved, and usually the copy is removed entirely (copy elision / NRVO). return std::move(local) can actually stop that optimisation.

Since C++17, when you return a temporary (prvalue) like return std::string("x"); or initialise from one, the copy or move is guaranteed not to happen at all: the object is built directly in its final place. This is guaranteed copy elision.

Using moves in everyday code

cpp
std::vector<std::string> names;
std::string line = readLine();
names.push_back(std::move(line));     // line is not needed any more: move it in

class Student {
public:
    explicit Student(std::string name) : name_(std::move(name)) {}   // "sink" parameter
private:
    std::string name_;
};
Take a sink parameter by value, then std::move it into the member: callers with temporaries pay one move, callers with lvalues pay one copy.
Careful: Do not use an object after moving from it, except to destroy it or assign it a new value. A moved-from std::string is valid but its content is unspecified. Also, std::move on a const object silently COPIES, because a const object cannot be stolen from.

Rule of zero, again

If your class only holds members like std::string, std::vector and std::unique_ptr, the compiler generates correct move operations for you. You write move constructors yourself only in low-level resource classes. Note that declaring a destructor or copy operation yourself stops the compiler from generating moves, and then the class silently copies instead of moving.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Move semantics: steal instead of copy · 1/7

Move semantics

Move semantics let C plus plus steal the insides of an object instead of copying them.

Prefer a video by a teacher? Find free videos on “c++ move semantics rvalue reference std::move explained” on YouTube ↗

In real life: Building a big report without copies

A reporting service builds a CSV of lakhs of rows as a std::string and returns it from a function, then puts it into an outbox vector to be emailed. Thanks to move semantics, the huge string is never copied: the return is elided and the push_back takes over its buffer.

cpp
#include <iostream>
#include <string>
#include <vector>

std::string buildCsv(int rows) {
    std::string csv = "order_id,amount\n";
    csv.reserve(static_cast<std::size_t>(rows) * 16);
    for (int i = 1; i <= rows; ++i) csv += std::to_string(i) + ",499\n";
    return csv;                              // moved or elided, never copied
}

int main() {
    std::vector<std::string> outbox;
    std::string report = buildCsv(100000);
    outbox.push_back(std::move(report));     // steal the buffer
    std::cout << outbox[0].size() << ' ' << report.size() << '\n';   // 988911 0
}
After the move, report is typically empty (unspecified by the standard, empty in practice for std::string).

Practice (0/4 done)

Task 1 · Question

What does std::move(x) actually do?

Task 2 · Question

std::string a = "data"; std::string b = std::move(a); What is guaranteed about a afterwards?

Task 3 · Question

const std::vector<int> v(1000000); auto w = std::move(v); What happens?

Task 4 · Practice

Write a class Playlist holding std::vector<std::string> songs. Give it a constructor that takes std::vector<std::string> by value and moves it into the member, and a function add(std::string song) that moves the song in. In main, show with size() that the source vector is empty after you std::move it into a Playlist.

Quick quiz

Question 1

1.Which is an rvalue?

Question 2

2.Why should move constructors be noexcept?

Question 3

3.What is the best way to return a large local vector from a function?

Question 4

4.A class declares only a destructor (~T() {}) and holds a std::vector. What happens to its moves?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which is an rvalue?

Answer: The result of a + b

a + b produces a temporary value with no name.

Why should move constructors be noexcept?

Answer: So std::vector moves elements when it grows instead of copying them

vector only uses a move if it cannot throw, to keep its exception guarantee.

What is the best way to return a large local vector from a function?

Answer: return v;

Plain return allows copy elision and is automatically treated as a move otherwise.

A class declares only a destructor (~T() {}) and holds a std::vector. What happens to its moves?

Answer: The implicit move operations are not declared, so moves silently become copies

A user-declared destructor suppresses the implicit move constructor and move assignment.

Which pair makes a deep copy automatically when you copy the object?Related

Answer: A std::vector member

std::vector copies all its elements into new memory.

From Copying objects: the rule of three, five and zero

A noexcept function throws. What happens?Related

Answer: std::terminate is called

Breaking a noexcept promise ends the program.

From Exceptions: throw, try, catch and noexcept

How do you pass ownership of a unique_ptr into a function sink(std::unique_ptr<T> p)?Related

Answer: sink(std::move(p))

Ownership must be moved explicitly.

From Smart pointers 1: std::unique_ptr

What is wrong with: const std::string& name() { std::string s = "Asha"; return s; }Related

Answer: It returns a reference to a local that is destroyed: a dangling reference

s dies when the function returns. Return std::string by value.

From References: another name for a variable

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Templates, smart pointers and move semantics”. The module exam comes next.
  2. Module 7 exam: Templates, smart pointers and move semantics · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Modern C++: lambdas, C++17 and C++20 essentials. 42 topics left in the course before the final exam.

← Previous: Smart pointers 2: std::shared_ptr and std::weak_ptr