Skip to content

5. Copy, Move & the Rule of Five

What & why

When you write b = a or pass an object by value, C++ calls special member functions to copy it. Since C++11 it can also move it — steal its resources instead of duplicating them. For most classes the compiler generates these correctly. For classes that manage a resource directly, you must get them right, or you'll get double frees, leaks, and shallow-copy bugs.

The six special member functions

class T {
public:
    T();                              // default constructor
    ~T();                             // destructor
    T(const T& other);                // copy constructor
    T& operator=(const T& other);     // copy assignment
    T(T&& other) noexcept;            // move constructor
    T& operator=(T&& other) noexcept; // move assignment
};
T a;             // default constructor
T b = a;         // copy constructor (b is new)
T c{a};          // copy constructor
b = a;           // copy assignment (b already exists)
T d = std::move(a);   // move constructor
b = std::move(c);     // move assignment

The rule of zero

If your class only holds members that already manage themselves (std::string, std::vector, std::unique_ptr, other well-behaved classes), don't declare any of the six. The compiler-generated versions copy/move each member, which is exactly right.

class Contact {
    std::string name_;
    std::vector<std::string> phones_;
    // no destructor, no copy/move functions — all correct automatically
};

This is the goal for 95% of your classes.

When the defaults go wrong

A class holding a raw owning pointer:

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

void oops() {
    Buffer a{10};
    Buffer b = a;    // default copy: copies the POINTER, not the ints
}                    // both destructors delete[] the same memory → double free

The rule of three / five

If a class needs a custom destructor, it almost certainly needs custom copy operations (rule of three), and should have move operations too (rule of five).

#include <algorithm>
#include <cstddef>
#include <utility>

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

    ~Buffer() { delete[] data_; }

    // Copy constructor: deep copy
    Buffer(const Buffer& other) : size_{other.size_}, data_{new int[other.size_]} {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // Copy assignment: copy-and-swap (strong exception safety, self-assignment safe)
    Buffer& operator=(const Buffer& other) {
        Buffer tmp{other};
        swap(tmp);
        return *this;
    }

    // Move constructor: steal, leave the source empty but valid
    Buffer(Buffer&& other) noexcept
        : size_{std::exchange(other.size_, 0)}, data_{std::exchange(other.data_, nullptr)} {}

    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        Buffer tmp{std::move(other)};
        swap(tmp);
        return *this;
    }

    void swap(Buffer& 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_;
};

std::exchange(x, new_value) sets x to new_value and returns the old value — perfect for "take it and leave nothing behind".

Or just don't

The better fix is to make the class rule-of-zero by using a managing member:

class Buffer {
public:
    explicit Buffer(std::size_t n) : data_(n) {}
private:
    std::vector<int> data_;   // copies, moves, and frees itself
};

Write the five yourself only when you're implementing a low-level resource wrapper.

Moves, lvalues, and rvalues

  • An lvalue has a name and an address you can take: x, v[0], *p.
  • An rvalue is a temporary about to disappear: 42, a + b, make_widget().

Moving from rvalues is always safe — nobody will look at them again. So the compiler moves from rvalues automatically:

std::vector<std::string> names;
names.push_back(std::string(1000, 'x'));   // temporary → moved, not copied

std::move(x) doesn't move anything. It's a cast that says "treat x as an rvalue; I'm done with it":

std::string big(1'000'000, 'x');
names.push_back(big);              // copies 1 MB
names.push_back(std::move(big));   // moves: just steals the pointer
// big is now "valid but unspecified" — typically empty. Don't use its value; you may assign to it.

Deleting and defaulting

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;             // forbid copy
    NonCopyable& operator=(const NonCopyable&) = delete;
    NonCopyable(NonCopyable&&) = default;                 // allow move
    NonCopyable& operator=(NonCopyable&&) = default;
};

std::unique_ptr, std::thread, std::ifstream, and std::mutex are all non-copyable. A class containing one is automatically non-copyable too.

What the compiler generates — the practical rules

  • Declare any copy operation, move operation, or destructor → the compiler stops generating move operations. Copies then silently happen instead of moves.
  • Declare a move operation → copy operations are deleted.

So if you declare one, think about all five — or declare none.

Why noexcept on moves matters

std::vector grows by allocating new storage and transferring elements. If your move constructor might throw, vector copies instead, to keep its strong exception guarantee. Mark moves noexcept (and make sure they don't throw).

Passing and returning — putting it together

// Returning a local: moved or elided automatically. Never write `return std::move(local);`
std::vector<int> make() {
    std::vector<int> v(1000);
    return v;
}

// "Sink" parameter: take by value, then move into place
class Person {
public:
    explicit Person(std::string name) : name_{std::move(name)} {}
private:
    std::string name_;
};

std::string n = "Ada";
Person p1{n};              // one copy (into the parameter), one move
Person p2{"Grace"};        // no copies at all
Person p3{std::move(n)};   // no copies

Common mistakes

  • Using an object after std::move as if it still had its value.
  • Writing a destructor and forgetting copy/move — rule of three/five violations.
  • return std::move(local); — disables copy elision. Just return local;.
  • std::move on a const object — silently copies (you can't steal from const).
  • Move constructors not marked noexcept — containers copy instead.
  • Self-assignment bugs in hand-written copy assignment — copy-and-swap avoids them.

Exercises

  1. Add print statements to all six special members of a class and predict the output for: construction, T b = a;, b = a;, T c = std::move(a);, pushing into a std::vector (with and without reserve), and returning from a function.
  2. Remove noexcept from the move constructor and watch std::vector::push_back start copying on reallocation.
  3. Implement the Buffer class above, then test it under -fsanitize=address: copy, move, self-assign, and move-from-then-assign.
  4. Rewrite Buffer using std::unique_ptr<int[]>. Which special members can you now delete? Which do you still need for copying?
  5. Write a Person constructor both as const std::string& and as by-value + std::move. Count copies in each case for lvalue and rvalue arguments.

Recap

  • Six special members: default ctor, destructor, copy ctor/assign, move ctor/assign.
  • Rule of zero: build classes from self-managing members and declare none of them.
  • Rule of five: if you must write a destructor for a resource, write (or delete) all five.
  • std::move is a cast, not a move; moved-from objects are valid but unspecified.
  • Mark moves noexcept. Return locals by value, never return std::move(x);.

Next: Operator overloading →