C++ / a working model

22 / 163   ·   C++11   ·   10 min

Deep copy, shallow copy, and the Rule of Zero / Five

Keep this sentence

Default copy proceeds memberwise; whether underlying resources are shared depends on the member types, not on a single deep-copy or shallow-copy label. Prefer letting standard resource types manage ownership; only when custom copy semantics are truly required should copy, move, assignment, and destruction be designed together.

In this lesson
  1. Copying members is not the same as copying resources
  2. Let members take ownership first
  3. Custom deep copy must include failure semantics
  4. Example
  5. Exercise

Copying members is not the same as copying resources

A compiler-generated copy usually copies bases and members one by one. Copying a raw pointer copies only the address, not the pointed-to object; copying a vector uses vector's value-semantic copy and gets independent element storage; copying a shared_ptr intentionally shares ownership. Judging deep versus shallow copy therefore requires walking the ownership semantics of the members.

If a class exclusively owns a resource through a raw pointer but only customizes a delete destructor, default copy lets multiple objects believe they each uniquely own the same resource and eventually double-free it. Not every shallow copy is wrong: a non-owning observer pointer is supposed to point at the same external object, but it must be clear that it cannot extend the observed object's lifetime.

Let members take ownership first

The Rule of Zero means a business class usually need not declare copy, move, assignment, or destruction itself; members such as string, vector, and unique_ptr do the work. A value object that contains a vector is naturally copyable; an object that contains a unique_ptr is naturally non-copyable but movable, which is often the desired contract.

The Rule of Five reminds resource types to coordinate the five special members; it does not require a handwritten implementation of each: they may be defaulted or deleted. A user-declared destructor, including one written = default, affects implicit move generation; and an expression that has a move constructor does not mean the underlying resource is necessarily actually moved.

Custom deep copy must include failure semantics

The example DeepBox needs exclusive storage and independent copies, so copy construction allocates a new integer and assignment constructs a temporary copy then swaps. If allocation fails, the original target is unchanged; self-assignment also does not destroy the data. Move is handed directly to unique_ptr, and the empty moved-from state is defined through value() as zero.

Copy-and-swap is easy to reason about, but it may give up reuse of existing capacity and must not be treated as a performance template for every container assignment. A real resource class should also make clear whether copying is expensive, whether moving can be non-throwing, and which operations remain valid after a move, so callers do not mistake a "valid state" for "still holding the original value."

Pitfalls

  • memcpy is not a general object-copying scheme; types with resource ownership or virtual functions especially must not replace special members with a byte copy.
  • After writing a destructor, do not assume the compiler still generates move operations automatically; recheck availability of all five special members.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <memory>
#include <utility>
#include <vector>

struct Values {
    std::vector<int> items;
};

class DeepBox {
    std::unique_ptr<int> value_;
public:
    explicit DeepBox(int value) : value_(new int(value)) {}
    DeepBox(const DeepBox& other)
        : value_(other.value_ ? new int(*other.value_) : nullptr) {}
    DeepBox& operator=(const DeepBox& other) {
        DeepBox temporary(other);
        value_.swap(temporary.value_);
        return *this;
    }
    DeepBox(DeepBox&&) noexcept = default;
    DeepBox& operator=(DeepBox&&) noexcept = default;
    ~DeepBox() = default;
    int value() const { return value_ ? *value_ : 0; }
    void set(int value) {
        if (value_) *value_ = value;
        else value_.reset(new int(value));
    }
};

int main() {
    Values a{{1, 2}};
    Values b = a;
    b.items[0] = 8;
    assert(a.items[0] == 1);
    DeepBox original(3);
    DeepBox copy = original;
    copy.set(7);
    assert(original.value() == 3 && copy.value() == 7);
    original = original;
    DeepBox moved = std::move(copy);
    assert(original.value() == 3);
    assert(moved.value() == 7 && copy.value() == 0);
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-copy.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

If DeepBox's semantics change to transfer-only with no copying, what is the simplest Rule of Zero version?

Show a reference answer

Keep the unique_ptr<int> member, the ordinary constructor, and the query functions, and remove the handwritten or explicitly defaulted special-member declarations from the example. unique_ptr makes implicit copy unavailable and supports implicit move; no extra destructor declaration is needed. If the business still wants an explicit delete to emphasize the interface, the required move operations must also be restored explicitly, which is no longer a strict Rule of Zero form.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog