2Brute2Best
πŸ”₯0
M2.2

Deep vs Shallow Copy & The Rule of 3

Medium High-frequency interview topic Pro OnlyCopy ConstructorCopy AssignmentSelf-Assignment TrapCopy-and-Swap IdiomRule of 3
⚑ 15-Second Mental ModelM2.2 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Core Intuition)

In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.

Rule to Remember: Make illegal states impossible to compile or run.
3 Golden Facts to Remember
Fact #1
What is the Rule of 3 in C++, and why does it exist?

The Rule of 3 dictates that if a class requires a user-defined Destructor, Copy Constructor, or Copy Assignment Operator, it almost certainly requires all three. This is because a custom destructor indicates manual management of an underlying resource (heap memory, OS file handle, socket). If you rely on compiler-generated copy operations for a resource-owning class, the compiler performs a memberwise shallow copy, causing multiple instances to share the same raw resource, leading to double-free crashes and data races upon destruction.

Fact #2
What is the Copy-and-Swap idiom, and what are its primary advantages?

The Copy-and-Swap idiom implements operator=(T other) by taking the right-hand operand by value (leveraging the copy constructor to create a stack temporary) and then swapping its state with *this using a non-throwing swap function (noexcept). Its advantages are: (1) Strong Exception Guarantee: If copying fails during argument initialization, *this is untouched; (2) Automatic self-assignment safety: It naturally handles a = a without an explicit pointer check; (3) Zero code duplication: It reuses the allocation and copy logic already written in the copy constructor.

Fact #3
Why must operator= return a reference to *this (T&) rather than void or T by value?

Returning T& preserves standard C++ operator chaining semantics, such as a = b = c; and conditional expressions like (a = b).size(). Returning void prohibits chaining. Returning by value (T) causes a needless copy constructor invocation and heap allocation for every assignment, destroying performance and returning an anonymous temporary rather than a reference to the actual mutated object.

01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

Think of a shallow copy like photocopying an apartment key card: both people now share the exact same room. If one tenant moves out and destroys the lock, the second tenant is left locked outβ€”or walking into rubble. A deep copy builds an identical apartment next door.

Interviewers test the Rule of 3 to see if you can manage real OS memory without crashing. Weak candidates recite the rule as a slogan ('if you write a destructor, write copies') without understanding heap allocator corruption. Senior engineers demonstrate three mechanical skills:

  • Double-Free Diagnostics: Tracing how bitwise pointer copying leads two destructors to call delete[] on the identical heap chunk, triggering allocator aborts (SIGABRT).
  • Self-Assignment & Memory Safety: Proving why naive operator= deletes old data too early, causing use-after-free bugs on a = a or leaving corrupted zombies if new throws std::bad_alloc.
  • The Copy-and-Swap Idiom: Writing clean, bulletproof code that provides the Strong Exception Guarantee and natural self-assignment safety with zero duplicated logic.
02

Under the hood & memory layout

Hardware & memory representation

When a class does not define custom copy operations, the compiler generates a bitwise memberwise shallow copy (memcpy). If that class holds a heap pointer (int* data_ = new int[N]), shallow copying produces catastrophic memory corruption in three distinct steps:

  1. Address Duplication: The compiler copies the 8-byte pointer address bit-by-bit. Two independent stack objects now hold the identical virtual memory address.
  2. The First Destruction: When the first object goes out of scope, its destructor runs delete[] data_. The OS allocator reclaims the memory block and marks it free in its internal chunk tables.
  3. The Fatal Second Destruction: When the second object goes out of scope, its destructor runs delete[] data_ on the already-freed block. The allocator detects corrupted boundary metadata and aborts the process immediately with a SIGABRT double-free crash.

A custom deep copy constructor solves this by requesting an entirely new heap block from the allocator and copying the data elements across, ensuring complete ownership isolation.

Stack Object Footprint

24 bytes on 64-bit systems: size_ (8 bytes), capacity_ (8 bytes), and raw pointer data_ (8 bytes). Shallow copying duplicates these 24 bytes bitwise without touching heap memory.

Heap Allocation Chunk

Heap allocators prepend an internal chunk header (8 to 16 bytes) storing chunk size and allocation flags. When delete[] runs twice on the same pointer, the allocator detects duplicate free-list insertion and aborts.

Self-Assignment Memory Hazard (a = a)

In a naive operator=, executing delete[] data_; data_ = new int[other.size_]; when this == &other frees other.data_ before reading from it. Reading from other.data_ becomes an immediate use-after-free (UAF).

Copy-and-Swap Idiom Stack Staging

Passing the right-hand operand by value constructs a temporary object on the stack using the copy constructor. If allocation throws std::bad_alloc, the exception escapes before *this is touched, providing the Strong Exception Guarantee.

03

Core concepts & syntax traps

01

Compiler-Generated Memberwise Shallow Copy & The Double-Free Hazard

If you omit a copy constructor, the compiler synthesizes one that performs member-by-member copy initialization.

For primitive types and raw pointers (int*), this copies the raw numeric addressβ€”not the heap buffer behind it:

  1. Shared Ownership: Both objects silently alias the exact same heap memory block.
  2. Unintended Mutation: Modifying elements through one object silently mutates the other.
  3. Double-Free Crash: When both objects exit their scopes, both invoke delete[] on the identical address, crashing the program instantly.
C++23 snippet
class ShallowOnly {
    int* ptr_;
public:
    ShallowOnly(int val) : ptr_(new int(val)) {}
    ~ShallowOnly() { delete ptr_; }
    // Compiler generates: ShallowOnly(const ShallowOnly& o) : ptr_(o.ptr_) {}
};

void test() {
    ShallowOnly a(42);
    ShallowOnly b = a; // Both a.ptr_ and b.ptr_ point to same memory!
} // Scope exit: a frees ptr_, b frees ptr_ again -> CRASH: double free detected!
Interview takeaway:

Any class owning a raw resource must explicitly manage or delete copy operations; default memberwise copies on raw pointers are lethal.

02

The Rule of 3: Why Destructor, Copy Constructor, and Copy Assignment Co-Exist

The Rule of 3 is a core C++ lifetime invariant: If your class requires a custom Destructor, Copy Constructor, or Copy Assignment Operator, it almost certainly requires all three.

Here is why they are inseparable:

  1. The Destructor Signal: A custom destructor means your class manages a manual resource (heap buffer, socket, file descriptor) that requires explicit cleanup.
  2. The Compiler Default Hazard: If you write the destructor but let the compiler synthesize copy operations, it performs shallow copies that create shared ownership of your private resource.
  3. The Complete Triad: You must implement all three to maintain safety:
    • Destructor: Frees the acquired resource on scope exit.
    • Copy Constructor: Allocates a brand-new independent resource when initializing a new object.
    • Copy Assignment Operator: Replaces an existing object's resource with an independent clone of another.
C++23 snippet
class DynamicArray {
    int* data_;
    std::size_t size_;
public:
    // 1. Destructor
    ~DynamicArray() noexcept { delete[] data_; }
    // 2. Copy Constructor (Deep Copy)
    DynamicArray(const DynamicArray& other) : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    // 3. Copy Assignment Operator (Guarded & Safe)
    DynamicArray& operator=(const DynamicArray& other);
};
Interview takeaway:

If your class frees a resource in its destructor, never let the compiler synthesize copy operations for you.

03

The Self-Assignment Trap (a = a) & The Use-After-Free Vulnerability

Self-assignment happens when an object is assigned to itself (a = a), either directly or through aliased references (x = y where &x == &y).

A naive operator= creates a severe Use-After-Free hazard:

  1. Premature Deletion: If the assignment operator starts with delete[] data_;, it destroys the resource of this.
  2. Self-Destruction: Because this and &other point to the identical object, other.data_ is now also destroyed!
  3. Reading Garbage: When the next line runs std::copy(other.data_, ...), it reads from deallocated memory.

Always guard assignment against identity (if (this == &other) return *this;) or use the Copy-and-Swap idiom.

C++23 snippet
// BUGGY ASSIGNMENT OPERATOR
DynamicArray& DynamicArray::operator=(const DynamicArray& other) {
    delete[] data_; // BUG: If this == &other, other.data_ is now FREED!
    data_ = new int[other.size_];
    std::copy(other.data_, other.data_ + other.size_, data_); // Reads dead memory!
    size_ = other.size_;
    return *this;
}

// CORRECT GUARD
DynamicArray& DynamicArray::operator=(const DynamicArray& other) {
    if (this == &other) return *this; // Essential pointer comparison!
    // ... allocate first, then delete ...
}
Interview takeaway:

Always guard against self-assignment (if (this == &other) return *this;) or use the Copy-and-Swap idiom.

04

Exception Safety in operator=: Allocate Before Deleting

Even with a self-assignment guard, writing delete[] data_; data_ = new int[other.capacity_]; is a critical reliability hazard.

  1. The Allocation Risk: If the system runs low on memory, new can throw std::bad_alloc.
  2. The Zombie Object Trap: If you deleted data_ before allocating the replacement, your object is now left in an invalid, broken state with a dead pointer. The caller's existing state is permanently destroyed.
  3. Commit-or-Rollback: To achieve the Strong Exception Safety Guarantee, always allocate and populate the replacement buffer first. Only when allocation succeeds do you delete the old buffer and update pointers.
C++23 snippet
DynamicArray& DynamicArray::operator=(const DynamicArray& other) {
    if (this == &other) return *this;
    
    // 1. Allocate new resource FIRST (may throw std::bad_alloc)
    int* newData = new int[other.capacity_];
    std::copy(other.data_, other.data_ + other.size_, newData);
    
    // 2. Only delete old resource after new resource is guaranteed!
    delete[] data_;
    
    // 3. Update pointers (cannot throw)
    data_ = newData;
    size_ = other.size_;
    capacity_ = other.capacity_;
    return *this;
}
Interview takeaway:

Never destroy an existing valid state until all operations that could throw an exception have succeeded.

05

The Copy-and-Swap Idiom: Strong Exception Safety with Zero Boilerplate

The Copy-and-Swap Idiom is the cleanest, most robust pattern for copy assignment in C++.

It accomplishes self-assignment safety and exception safety in three seamless steps:

  1. Pass-by-Value Staging: The operator takes its parameter (DynamicArray other) by value. This invokes the copy constructor before entering the function. If allocation fails, the exception escapes before *this is ever touched (Strong Exception Guarantee).
  2. O(1) Register Swap: Inside the operator, swap(*this, other) swaps the raw pointer and size members in CPU registers without allocating or throwing.
  3. Automatic Cleanup: When the operator finishes and returns, parameter other leaves scope. Its destructor automatically cleans up your old heap memory!
C++23 snippet
class DynamicArray {
    int* data_{nullptr};
    std::size_t size_{0};
public:
    friend void swap(DynamicArray& first, DynamicArray& second) noexcept {
        using std::swap;
        swap(first.data_, second.data_);
        swap(first.size_, second.size_);
    }
    
    // Pass-by-value triggers copy constructor!
    DynamicArray& operator=(DynamicArray other) noexcept {
        swap(*this, other); // Steal new data, give old data to 'other'
        return *this;        // 'other' destructor cleans up old data!
    }
};
Interview takeaway:

Copy-and-Swap provides the Strong Exception Guarantee and automatic self-assignment safety with zero code duplication.

Brute2Best Pro Content

Unlock Live Implementation & Interview Scenarios

The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering