Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 9 of 12

Copying a large object is expensive. Often the source is a temporary that is about to be destroyed, so the copy is pure waste — we allocate a new array, duplicate every element, then throw the original away.

Moving takes the resource instead of duplicating it.

COPY                              MOVE
  a: [ptr] --> [1 2 3]              a: [ptr] --> [1 2 3]
  b: [ptr] --> [1 2 3]  (new)       b: [ptr] ----^
                                    a: [null]     (a gives it up)

A copy duplicates the array. A move reassigns one pointer and leaves the source empty. For a million-element vector that is the difference between a million copies and one pointer write.

Writing them

#include <iostream>
#include <utility>

class Buffer {
    int* data;
    std::size_t count;

public:
    Buffer(std::size_t n) : data(new int[n]), count(n) {
        for (std::size_t i = 0; i < count; ++i) {
            data[i] = 0;
        }
    }

    ~Buffer() { delete[] data; }

    Buffer(const Buffer& other) : data(new int[other.count]), count(other.count) {
        for (std::size_t i = 0; i < count; ++i) {
            data[i] = other.data[i];
        }
        std::cout << "  copy constructor\n";
    }

    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data;
            count = other.count;
            data = new int[count];
            for (std::size_t i = 0; i < count; ++i) {
                data[i] = other.data[i];
            }
        }
        std::cout << "  copy assignment\n";
        return *this;
    }

    // Move constructor: take the pointer, leave the source empty but valid.
    Buffer(Buffer&& other) noexcept : data(other.data), count(other.count) {
        other.data = nullptr;
        other.count = 0;
        std::cout << "  move constructor\n";
    }

    // Move assignment: release ours, take theirs, empty them out.
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            count = other.count;
            other.data = nullptr;
            other.count = 0;
        }
        std::cout << "  move assignment\n";
        return *this;
    }

    std::size_t size() const { return count; }
    void set(std::size_t i, int v) { data[i] = v; }
    int get(std::size_t i) const { return data[i]; }
};

int main() {
    Buffer a(3);
    a.set(0, 42);

    std::cout << "copy:\n";
    Buffer b = a;

    std::cout << "move:\n";
    Buffer c = std::move(a);

    std::cout << "c[0]=" << c.get(0) << " c.size=" << c.size() << "\n";
    std::cout << "a was moved from, a.size=" << a.size() << "\n";
    return 0;
}
copy:
  copy constructor
move:
  move constructor
c[0]=42 c.size=3
a was moved from, a.size=0

c took a's array. a is now empty — still a valid object, still destructible, just holding nothing.

&& — an rvalue reference

Buffer&& binds only to things that are about to expire: temporaries, and objects you explicitly mark with std::move. That is how the compiler picks the move overload over the copy one.

#include <iostream>

void f(const int& x) { std::cout << "lvalue ref: " << x << "\n"; }
void f(int&& x) { std::cout << "rvalue ref: " << x << "\n"; }

int main() {
    int a = 1;

    f(a);              // a has a name and persists -> lvalue
    f(2);              // a temporary -> rvalue
    f(a + 1);          // the result is a temporary -> rvalue
    return 0;
}
lvalue ref: 1
rvalue ref: 2
rvalue ref: 2

Roughly: an lvalue has a name you could take the address of; an rvalue is a temporary. If the source is a temporary, stealing from it is safe, because nobody can observe it afterwards.

Two rules for the moved-from object

It must be left valid. Not correct, not useful — valid, meaning its destructor runs safely and you may assign a new value to it. Setting data = nullptr matters: delete[] nullptr is explicitly defined as a no-op, so the source's destructor is harmless.

You may not assume what it holds. After std::move(a), the only safe operations on a are destroying it or assigning to it. Do not read it. For our Buffer we know it is empty; for a std::string from the standard library the state is "valid but unspecified."

Why noexcept is not decoration

Buffer(Buffer&& other) noexcept

That keyword has real consequences. When std::vector grows, it must relocate its elements. It will only move them if the move constructor is noexcept — otherwise it copies, because a throw halfway through relocating would leave the vector unrecoverable.

So a move constructor that is not noexcept is silently ignored by the container that would benefit most. Move operations do not allocate, so they have no reason to throw. Mark them noexcept.

Where moves happen without you asking

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

std::vector<std::string> build() {
    std::vector<std::string> v;
    v.push_back("alpha");
    v.push_back("beta");
    return v;                          // moved out, not copied
}

int main() {
    std::vector<std::string> names = build();
    std::cout << names.size() << " " << names[0] << "\n";

    std::vector<std::string> more;
    more.push_back(std::string("gamma"));    // temporary is moved in

    std::cout << more[0] << "\n";
    return 0;
}
2 alpha
gamma

Returning a local by value, pushing a temporary into a container, passing a temporary to a function — all move automatically when a move constructor exists. You mostly benefit from move semantics without writing anything.

Try it

Add a counter to Buffer that records how many ints have been copied element-by-element. Build a vector of several Buffers and compare the count with and without move operations.

Checkpoint

  • A move transfers ownership of a resource instead of duplicating it.
  • T&& is an rvalue reference and binds only to temporaries and std::moved objects.
  • The moved-from object must be left valid — destructible and assignable.
  • After moving from an object, only destroy or reassign it; do not read it.
  • Mark move operations noexcept, or containers will copy instead of moving.
  • Returning locals and passing temporaries move automatically once you have the operations.

Questions about this lesson

Log in to ask a question.