Phase 2 · Classes, Ownership & Resource SafetyModule 11~54 min read

Copying, Moving & the Rule of Zero

Design predictable value types using generated special members, copying, moving, and resource-owning members.

What you'll learn

C++ types can behave like independent values, exclusive owners, or deliberately shared handles. Copy and move operations encode that policy. The safest design is usually the Rule of Zero: compose members that already know how to copy, move, and clean themselves up.

By the end, you'll be able to:

  • Identify copy/move construction and copy/move assignment
  • Explain std::move, valid moved-from states, and noexcept moves
  • Apply the Rules of Zero, Five, and Three
  • Design predictable value semantics without double ownership

The special member functions

Construction creates a new object; assignment changes an existing object. Copy operations read from an lvalue source, while move operations may transfer resources from an expiring non-const source. A destructor completes the set that controls value lifetime.

OperationTypical declarationWhen used
Copy constructorT(const T&)New object from an existing value
Copy assignmentT& operator=(const T&)Existing object receives a value
Move constructorT(T&&)New object takes transferable state
Move assignmentT& operator=(T&&)Existing object releases then takes state
Destructor~T()Object lifetime ends

Copying and independent values

A correct copy has independent value behavior: changing the copy does not unexpectedly mutate the original. Standard containers and strings perform deep value copies of their elements, so a class composed from them usually receives correct generated copy operations.

copy_value.cpp
#include <iostream>
#include <string>
#include <vector>

struct Playlist {
    std::string name;
    std::vector<std::string> tracks;
};

int main() {
    Playlist original{"Focus", {"Dawn", "Flow"}};
    Playlist copy{original};
    copy.tracks.push_back("Night");

    std::cout << original.tracks.size() << ' '
              << copy.tracks.size() << '\n';
}

Key idea

Do not write a copy constructor merely because a class is important. Let the compiler compose the correct operations when the members already have the desired semantics.

Moving and transfer

A move operation may transfer expensive resources instead of duplicating them.std::move does not move by itself; it casts an expression to an rvalue so move overloads become eligible. The selected constructor or assignment performs the transfer.

move_values.cpp
#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::vector<std::string> pending{"parse", "compile", "test"};
    std::vector<std::string> active{std::move(pending)};

    std::cout << active.size() << '\n';
    pending.clear(); // valid: its exact old contents are unspecified
    pending.push_back("package");
    std::cout << pending.front() << '\n';
}

Watch out

After a move, rely only on the type's documented post-move guarantees. Standard-library objects are valid but often have an unspecified value; assign, clear, destroy, or call operations whose preconditions are satisfied.

Rule of Zero

If a class does not directly manage a raw resource, define none of the five special operations. Use values, containers, strings, and smart pointers as members and let their behavior compose. This removes code, reduces exception-safety work, and keeps ownership visible.

rule_of_zero.cpp
#include <string>
#include <vector>

class Document {
public:
    Document(std::string title, std::vector<std::string> lines)
        : title_{std::move(title)}, lines_{std::move(lines)} {}

    const std::string& title() const { return title_; }

private:
    std::string title_;
    std::vector<std::string> lines_;
};

// No destructor, copy operation, or move operation is needed.

Rule of Five and exclusive owners

A type that directly owns a raw resource often needs a destructor and deliberate copy/move policy. If copying has no sensible meaning, delete it and support moves. A move should leave the source safe to destroy and should usually be noexcept so containers can use it while preserving strong guarantees.

movable_handle.cpp
#include <cstdio>
#include <utility>

class FileHandle {
public:
    explicit FileHandle(std::FILE* file = nullptr) : file_{file} {}
    ~FileHandle() { if (file_) std::fclose(file_); }

    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    FileHandle(FileHandle&& other) noexcept
        : file_{std::exchange(other.file_, nullptr)} {}

    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (file_) std::fclose(file_);
            file_ = std::exchange(other.file_, nullptr);
        }
        return *this;
    }

private:
    std::FILE* file_{};
};

Tip

In real code, first try to express the resource with unique_ptr plus a custom deleter. Then the wrapper can often return to the Rule of Zero.

Assignment and exception safety

Copy assignment must handle self-assignment, release the old value, and avoid losing it if creating the replacement throws. Copy-and-swap first creates a complete copy, then swaps it into place; the temporary later destroys the old state. It is simple and strongly safe, though a specialized assignment may reuse capacity more efficiently.

copy_swap.cpp
class Buffer {
public:
    friend void swap(Buffer& left, Buffer& right) noexcept {
        using std::swap;
        swap(left.data_, right.data_);
        swap(left.size_, right.size_);
    }

    Buffer& operator=(Buffer replacement) {
        swap(*this, replacement);
        return *this;
    }

private:
    int* data_{};
    std::size_t size_{};
};

// The full type must also define construction, copying, and destruction.
This pattern explains ownership mechanics; std::vector<int> should normally be the member instead.

Recap & quick check

Key takeaways

  • Construction creates a new object; assignment replaces the value of an existing object.
  • std::move enables move overload resolution but the selected operation performs the transfer.
  • Moved-from objects remain valid, though their exact values may be unspecified.
  • The Rule of Zero is the preferred outcome when standard members already manage resources.
  • Direct resource owners need explicit copy/move policy and usually noexcept move operations.

Quick check

1. Which operation initializes a new T from an existing lvalue T?

2. What does std::move do directly?

3. What is the Rule of Zero?

4. Why mark a move constructor noexcept when true?

Next: Module 12 — Operator Overloading & Conversions, where value types gain natural syntax without surprising their users.