Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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

  1. 1

    Go through the data members one by one and write down how each behaves. A std::string or std::vector copies deeply and moves cheaply. A std::unique_ptr moves but can't be copied. An int or double just copies. A raw pointer that the class deletes is the dangerous one: copying it copies only the address.

    Example

    Document has std::string title_; (copies deeply) and std::unique_ptr<Image> cover_; (move-only). §4's Buffer has int size_; (plain copy) and int* 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. 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 = default because 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: = delete the copy constructor and copy assignment. If every member moves correctly (library types such as std::vector, std::string or std::unique_ptr), = default the 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 = default the 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 a std::unique_ptr with a custom deleter (Appendix C §4); its own move already empties the source.

    Why: a copy that shouldn't exist is a design bug. = delete turns it into a compile error at the line that tries.

  3. 3

    Swap every raw owning pointer for a library type that owns: a std::vector for an array, a std::string for text, a std::unique_ptr for one object on the heap.

    Example

    In Buffer, replace int size_; int* data_; with std::vector<int> data_;. The constructor becomes explicit 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. 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, = default the moves so you keep them.

    Example

    A class that declares ~Shape() = default; for some other reason also writes Shape(Shape&&) = default; and Shape& operator=(Shape&&) = default;, plus the two copies with = default if it should stay copyable.

    Why: declaring a destructor or a copy operation stops the compiler from generating the moves, and std::move then falls back to a copy.

  5. 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); leaves a at 500 1 and b at 750 2, so the copy is independent. For Account, with the copies deleted, Account b = a; fails with GCC's use 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 = default the moves, so the class can't be moved either.
  • Writing = default moves 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 noexcept off hand-written moves, so std::vector copies the elements when it grows.