C++ Primer, Chapter 13

Copy Control

Copy constructor, copy assignment, destructor, move semantics, and the Rule of Five. What happens when objects are born, cloned, transferred, and destroyed.

Prerequisites: Chapter 7 (Classes) + Chapter 12 (Dynamic Memory).
10
Chapters
4+
Simulations

Chapter 0: Why Copy Control?

Consider a class that manages a dynamically allocated array. When you copy an object of that class, what happens to the array? Do you copy the pointer (both objects point to the same array)? Or do you copy the data (each object gets its own array)?

The answer depends on what you want. The compiler's default — copy every member — copies the pointer. Now two objects share the same array. Delete one, and the other has a dangling pointer. This is the shallow copy problem, and it's why copy control exists.

The five special members: A class controls what happens when objects are copied, moved, assigned, and destroyed through five special member functions: copy constructor, copy-assignment operator, destructor, move constructor, and move-assignment operator.
c++
class HasPtr {
    string *ps;
    int i;
public:
    HasPtr(const string &s = string())
        : ps(new string(s)), i(0) {}
    // Without copy control: compiler copies ps (pointer)
    // → two HasPtr objects share the same string!
    // → delete one → the other has a dangling pointer
};
What is the "shallow copy problem"?

Chapter 1: The Copy Constructor

A copy constructor initializes a new object as a copy of an existing object. Its first parameter is a reference to const of the class type:

c++
class Foo {
public:
    Foo();                     // default constructor
    Foo(const Foo&);            // copy constructor
};

When Is It Called?

Copy initialization happens in more places than you might expect:

SituationExample
Initialize with =string s2 = s1;
Pass by valuevoid f(string s);
Return by valuestring f() { return s; }
Brace-init a containervector<string> v = {"a", "b"};
Container insert/pushv.push_back(s);
Why must the parameter be a reference? If the copy constructor took its argument by value, calling it would require making a copy of the argument — which would call the copy constructor, which would need to copy the argument, which would call... infinite recursion. The reference breaks the cycle.

The Synthesized Copy Constructor

If you don't define one, the compiler generates a synthesized copy constructor that copies each non-static member using that member's own copy constructor (for class types) or direct copy (for built-in types). This is memberwise copy.

Why must the copy constructor's parameter be a reference?

Chapter 2: The Copy-Assignment Operator

The copy-assignment operator is called when you assign one existing object to another:

c++
class HasPtr {
public:
    HasPtr& operator=(const HasPtr &rhs) {
        auto newps = new string(*rhs.ps);  // 1. copy the resource
        delete ps;                        // 2. free old resource
        ps = newps;                       // 3. point to new copy
        i = rhs.i;
        return *this;                     // 4. return *this
    }
};
Self-assignment safety: Notice we copy the resource before deleting the old one. If we deleted first and rhs was the same object as *this (self-assignment: a = a), we'd destroy the data we're trying to copy. Always handle self-assignment correctly.

Copy-and-Swap Idiom

An elegant pattern that handles self-assignment and provides strong exception safety:

c++
class HasPtr {
    friend void swap(HasPtr&, HasPtr&);
public:
    // Note: parameter is by VALUE (makes a copy)
    HasPtr& operator=(HasPtr rhs) {  // copy made here
        swap(*this, rhs);            // swap our guts with the copy
        return *this;                // rhs destroyed, frees old data
    }
};
Why is it important to copy the resource before deleting the old one in operator=?

Chapter 3: The Destructor

The destructor is called when an object is destroyed. It releases resources acquired during the object's lifetime:

c++
class HasPtr {
public:
    ~HasPtr() { delete ps; }  // free the dynamic string
};

When Is the Destructor Called?

SituationWhat's destroyed
Variable goes out of scopeLocal/automatic objects
Object is deleteddelete p; destroys *p
Container is destroyedAll elements
Temporary expression endsTemporaries from expressions
shared_ptr ref count hits 0The managed object

The destructor body runs first, then members are destroyed in reverse order of declaration. For class-type members, their own destructors are called. For built-in types (pointers, ints), nothing happens — which is why raw pointers to dynamic memory need explicit delete.

The destructor body does the class-specific cleanup. Member destruction is automatic. If your class allocates resources in its constructor (dynamic memory, file handles, locks), the destructor is where you release them.
In what order are class members destroyed by the destructor?

Chapter 4: The Rule of Three/Five

If a class needs a custom destructor, it almost certainly needs a custom copy constructor and copy-assignment operator too. This is the Rule of Three.

The Rule of Three: If you define any one of {destructor, copy constructor, copy-assignment operator}, you should define all three. If the destructor frees a resource, the default copy operations will create shallow copies that share the resource — leading to double-free or use-after-free.

C++11 extended this to the Rule of Five: if you define any copy-control member, also define (or explicitly default/delete) the move constructor and move-assignment operator.

= default and = delete

c++
class NoCopy {
public:
    NoCopy() = default;                // use synthesized default ctor
    NoCopy(const NoCopy&) = delete;    // no copying allowed
    NoCopy& operator=(const NoCopy&) = delete;  // no assignment
    ~NoCopy() = default;              // use synthesized destructor
};
SyntaxMeaning
= defaultUse the compiler-generated version
= deletePrevent this operation entirely (compile error if called)
When to delete: Some types should never be copied. iostream objects, for example, cannot be copied — what would it mean to copy cout? Use = delete to make this intent explicit and get a compile error instead of a runtime bug.
If a class has a custom destructor but default (compiler-generated) copy constructor, what can go wrong?

Chapter 5: Value-like vs Pointer-like Classes

When designing copy control, you have two fundamental choices: should copies of your class behave like values or like pointers?

Value-likePointer-like
Copy meansDeep copy (independent copies)Shared state (reference counted)
Change one?Other is unaffectedBoth see the change
Examplesstring, vectorshared_ptr, iostream
Resource mgmtEach copy owns its ownShared ownership with ref count
Value vs Pointer Semantics

Create an object, copy it, then modify the original. Watch how value-like copies are independent while pointer-like copies share state.

Value-like Copy Control

c++
// Deep copy: each HasPtr owns its own string
HasPtr(const HasPtr &p) : ps(new string(*p.ps)), i(p.i) {}
HasPtr& operator=(const HasPtr &rhs) {
    auto newp = new string(*rhs.ps);
    delete ps;
    ps = newp; i = rhs.i;
    return *this;
}
In a value-like class, what does the copy constructor do with dynamically allocated members?

Chapter 6: Move Semantics

Copying is safe but expensive. When an object is about to be destroyed anyway (a temporary, or a variable being returned), why copy its resources? Just steal them. This is move semantics.

c++
class StrVec {
public:
    // Move constructor: steal resources from rhs
    StrVec(StrVec &&rhs) noexcept
        : elements(rhs.elements), first_free(rhs.first_free), cap(rhs.cap) {
        // Leave rhs in a valid but unspecified state
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }

    // Move assignment: free our resources, steal from rhs
    StrVec& operator=(StrVec &&rhs) noexcept {
        if (this != &rhs) {
            free();                   // free our existing resources
            elements = rhs.elements;  // steal rhs's resources
            first_free = rhs.first_free;
            cap = rhs.cap;
            rhs.elements = rhs.first_free = rhs.cap = nullptr;
        }
        return *this;
    }
};

swap: The Secret Weapon

Defining a custom swap for your class enables the copy-and-swap idiom and is used by standard algorithms. For classes that manage resources, swapping pointers is far cheaper than copying:

c++
class HasPtr {
    friend void swap(HasPtr&, HasPtr&);
    // ...
};
inline void swap(HasPtr &lhs, HasPtr &rhs) {
    using std::swap;
    swap(lhs.ps, rhs.ps);  // swap pointers, not string data
    swap(lhs.i, rhs.i);    // swap ints
}
Always call swap with using std::swap then an unqualified swap(a, b). This lets argument-dependent lookup find a type-specific swap if one exists, falling back to std::swap otherwise. Never write std::swap(a, b) directly — it bypasses type-specific optimizations.
After a move, the source is "valid but unspecified." You can destroy it or assign to it, but you shouldn't use its value. Think of it as an empty shell — its resources have been transferred to the new object.
noexcept is critical. Containers like vector only use move operations during reallocation if they're marked noexcept. If the move might throw, vector falls back to copying for safety. Always mark move operations noexcept if they don't throw.
Why should move operations be marked noexcept?

Chapter 7: Rvalue References

Move semantics need a way to detect objects that are safe to move from. Enter rvalue references, declared with &&:

c++
int i = 42;
int &r = i;           // lvalue reference: binds to lvalue (named object)
// int &&rr = i;       // ERROR: can't bind rvalue ref to lvalue
int &&rr = i * 42;    // OK: bind rvalue ref to temporary (rvalue)
int &&rr2 = std::move(i);  // OK: move "casts" lvalue to rvalue

Lvalues vs Rvalues

LvalueRvalue
IdentityHas a name/addressTemporary, no persistent identity
Examplesx, *p, v[0]42, x + y, f() return
Can bind toT& or const T&T&& or const T&
Safe to move?No (still in use)Yes (about to die)
std::move: Tells the compiler "I promise I won't use this object's value again, so treat it as a temporary." It doesn't actually move anything — it just casts an lvalue to an rvalue reference, enabling the move constructor/assignment to be called.
c++
// Without move: expensive copy
vector<string> v1 = getHugeVector();   // copies return value

// With move: cheap pointer swap
vector<string> v2 = std::move(v1);     // steals v1's guts
// v1 is now empty (valid but unspecified)
What does std::move actually do?

Chapter 8: Object Lifecycle Simulator

Let's trace every copy-control operation as objects are created, copied, moved, assigned, and destroyed. This is how you debug copy-control issues in real code.

Object Lifecycle Tracer

Perform operations and watch which special member functions are called. Each event appears in the log.

Observe: Move operations leave the source in an empty state (grayed out). Copy operations create fully independent objects. The destructor always runs when an object dies — scope exit, explicit delete, or container destruction.

Chapter 9: Beyond — Connections

Copy control is the mechanism that makes C++ classes behave like built-in types. Get it right, and your classes compose seamlessly. Get it wrong, and you get crashes, leaks, and corruption.

This ChapterBuilds Toward
Rule of FiveDesigning any class that owns resources
Move semanticsEfficient return-by-value, container resizing
Rvalue referencesPerfect forwarding in templates (Ch 16)
Value vs pointer classesDesigning class hierarchies (Ch 15)
Copy-and-swapException-safe assignment everywhere
Lippman's advice: "If a class needs a destructor, it almost certainly also needs the copy-assignment operator and a copy constructor." This is the Rule of Three. In modern C++, extend it to Five by also considering move operations.

Continue Reading

Next up: Chapter 15: Object-Oriented Programming — inheritance, virtual functions, and dynamic binding.