Recipe
Make a class copy and move correctly
Use when: you're designing or reviewing a class and need to decide what T b = a;, b = a; and std::move(a) should do to it
Goal: a class whose copies are independent (or forbidden on purpose), whose moves are cheap, and which declares as few of the five special members as possible
Taught in The Rule of Zero, = default and = delete
Quick recipe
-
1
Go through the data members one by one and write down how each behaves. A
std::stringorstd::vectorcopies deeply and moves cheaply. Astd::unique_ptrmoves but can't be copied. Anintordoublejust copies. A raw pointer that the classdeletes is the dangerous one: copying it copies only the address.Example
Documenthasstd::string title_;(copies deeply) andstd::unique_ptr<Image> cover_;(move-only). §4'sBufferhasint size_;(plain copy) andint* data_;(raw owner: shallow copy).Why: the compiler builds the class's copy and move out of its members' copy and move, so the members decide what the defaults do.
-
2
Ask what a second, independent object would mean.
Example
#include <vector> class Ledger { public: Ledger() = default; Ledger(const Ledger&) = delete; Ledger& operator=(const Ledger&) = delete; Ledger(Ledger&&) = default; Ledger& operator=(Ledger&&) = default; private: std::vector<int> entries_; };The moves need
= defaultbecause declaring the copy operations stops the compiler from generating them.- a copy is a sensible, separate thing (a list of scores, a name, a shape) — copies should work; carry on with Step 3.
- the object stands for something that must stay unique (a bank account, a file handle, a network connection) — forbid copying:
= deletethe copy constructor and copy assignment. If every member moves correctly (library types such asstd::vector,std::stringorstd::unique_ptr),= defaultthe two moves so it can still be handed over. - it must stay unique and holds a raw handle itself (a
std::FILE*, a socket number) — don't= defaultthe moves: the default copies the handle and leaves it in the source, so both destructors close it. Either write the two moves by hand so they empty the source (Step 3, §4). Or hold the handle in astd::unique_ptrwith a custom deleter (Appendix C §4); its own move already empties the source.
Why: a copy that shouldn't exist is a design bug.
= deleteturns it into a compile error at the line that tries. -
3
Swap every raw owning pointer for a library type that owns: a
std::vectorfor an array, astd::stringfor text, astd::unique_ptrfor one object on the heap.Example
In
Buffer, replaceint size_; int* data_;withstd::vector<int> data_;. The constructor becomesexplicit Buffer(int size) : data_(size) {}, and the destructor, copy and move code all go away.- every member can be swapped — the class follows the Rule of Zero; go to Step 4.
- the class really must hold a raw resource (it is the low-level owner) — it follows the Rule of Five: write the destructor, both copy operations and both moves, mark the moves
noexcept, and keep that class small so the hand-written code lives in one place.
Why: a member that manages itself makes the compiler's generated operations correct. Hand-written copy code is where double deletes and leaks come from.
-
4
For a Rule of Zero class, delete any destructor, copy or move you wrote and let the compiler generate them. If you must declare one for another reason,
= defaultthe moves so you keep them.Example
A class that declares
~Shape() = default;for some other reason also writesShape(Shape&&) = default;andShape& operator=(Shape&&) = default;, plus the two copies with= defaultif it should stay copyable.Why: declaring a destructor or a copy operation stops the compiler from generating the moves, and
std::movethen falls back to a copy. -
5
Write three quick checks. Copy an object, change the copy, and confirm the original didn't change. Move from an object and confirm the moved-from one is still safe to destroy. If copies are forbidden, confirm that
T b = a;fails to compile.Example
This section's
ScoreCard:ScoreCard b = a; b.add(250);leavesaat500 1andbat750 2, so the copy is independent. ForAccount, with the copies deleted,Account b = a;fails with GCC'suse of deleted function.Why: a shallow copy compiles without a warning, so only running the copy tells you whether the two objects are really separate.
Common mistakes
- Writing a destructor "to be safe" in a class whose members already clean up, which turns its moves into copies without any warning.
- Deleting the copy operations and forgetting to
= defaultthe moves, so the class can't be moved either. - Writing
= defaultmoves for a class that holds a raw handle: both the moved-to and the moved-from object then release the same handle. - Keeping a raw owning pointer and writing only the destructor: the generated copy then shares the array, and the second destructor deletes it again.
- Leaving
noexceptoff hand-written moves, sostd::vectorcopies the elements when it grows.