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::moveas 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. Justreturn local;.std::moveon aconstobject — silently copies (you can't steal fromconst).- Move constructors not marked
noexcept— containers copy instead. - Self-assignment bugs in hand-written copy assignment — copy-and-swap avoids them.
Exercises¶
- 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 astd::vector(with and withoutreserve), and returning from a function. - Remove
noexceptfrom the move constructor and watchstd::vector::push_backstart copying on reallocation. - Implement the
Bufferclass above, then test it under-fsanitize=address: copy, move, self-assign, and move-from-then-assign. - Rewrite
Bufferusingstd::unique_ptr<int[]>. Which special members can you now delete? Which do you still need for copying? - Write a
Personconstructor both asconst 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::moveis a cast, not a move; moved-from objects are valid but unspecified.- Mark moves
noexcept. Return locals by value, neverreturn std::move(x);.
Next: Operator overloading →