Senior 5 min · March 06, 2026

C++ Copy Constructor — Fixing Double-Free Shallow Copy Bugs

Double-free crash on exit? Default copy constructors copy pointer addresses, not data.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Copy constructor initialises a new object from an existing one. It defines what "copy" means for your class.
  • Compiler-generated copy does member-wise (shallow) copy. Safe for ints, dangerous for raw pointers.
  • Deep copy constructor allocates fresh memory and copies the data, making both objects independent.
  • In production, a missing or shallow copy causes double-free crashes that only show under load.
  • Performance: deep copy is O(n), move constructor is O(1). Always prefer move when ownership transfer works.
  • Biggest mistake: writing a destructor but forgetting the copy constructor and copy assignment — leads to corrupted heaps.
Plain-English First

Imagine you have a detailed blueprint for a house. When your friend wants to build the same house, they could either trace your blueprint (sharing the same paper) or make a completely fresh photocopy they can mark up independently. A copy constructor is C++'s way of making that fresh photocopy of an object — so the new one starts life as an identical twin but is completely independent. Without it, C++ might just hand your friend the original blueprint, and any changes they make would mess up your copy too.

Every serious C++ program eventually needs to duplicate objects — passing one to a function, returning one from a method, or storing one in a container. At that moment, C++ must answer a critical question: what does 'make a copy' actually mean for this specific class? For a simple integer that answer is obvious, but the moment your class owns a raw pointer, opens a file handle, or manages a network socket, a naive byte-for-byte duplicate can silently corrupt your program. That's the problem the copy constructor was designed to solve.

The copy constructor is a special member function that C++ calls whenever an object is initialised from another object of the same type. It lets you define exactly what 'copy' means for your class — whether that's a shallow mirror image, a fully independent deep clone, or something in between. Without a thoughtful copy constructor, two seemingly separate objects can end up sharing the same underlying memory, leading to double-free crashes, dangling pointers, and data corruption that only shows up at the worst possible moment.

By the end of this article you'll understand why the compiler-generated copy constructor is sometimes a ticking time bomb, how to write a correct deep-copying version, when to delete it entirely, and how to talk confidently about it in a technical interview. We'll build a realistic DocumentBuffer class step by step so every concept has immediate, tangible context.

What the Compiler Generates — and Why That's Sometimes Dangerous

If you don't write a copy constructor, the compiler generates one for you. This default version performs a member-wise copy: it copies each data member from the source object into the new object one by one. For value types like int, double, or std::string, that works perfectly because those types already know how to copy themselves correctly.

The danger arrives with raw pointers. If your class holds a char or int pointing to heap memory, a member-wise copy duplicates the pointer value — the memory address — not the data it points to. Both the original object and the new copy now point at the exact same block of heap memory. Neither object knows the other exists.

When the first one is destroyed, its destructor frees that memory. When the second one is later destroyed, its destructor tries to free memory that's already gone. That's undefined behaviour, and on most platforms it's an immediate crash.

This is called a shallow copy, and understanding it deeply is the single most important prerequisite for writing correct C++ classes that manage resources.

ShallowCopyDanger.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <cstring>

namespace io::thecodeforge::examples {

// A naive buffer class that does NOT define a copy constructor.
// The compiler will generate a shallow copy — watch what happens.
class NaiveBuffer {
public:
    char* data;   
    int   size;

    NaiveBuffer(const char* text) {
        size = static_cast<int>(std::strlen(text)) + 1;
        data = new char[size];           
        std::strcpy(data, text);         
        std::cout << "[Construct] Allocated buffer at address "
                  << static_cast<void*>(data) << "\n";
    }

    ~NaiveBuffer() {
        std::cout << "[Destroy]   Freeing buffer at address "
                  << static_cast<void*>(data) << "\n";
        delete[] data;                   
    }
};

}

int main() {
    using namespace io::thecodeforge::examples;
    NaiveBuffer original("Hello, CodeForge");

    // The compiler's shallow copy constructor runs here.
    NaiveBuffer copyOfBuffer = original;  

    std::cout << "Original address:  " << static_cast<void*>(original.data) << "\n";
    std::cout << "Copy address:      " << static_cast<void*>(copyOfBuffer.data) << "\n";

    // When main() exits, the double-free crash occurs here.
    return 0;
}
Output
[Construct] Allocated buffer at address 0x55a3c2e6aeb0
Original address: 0x55a3c2e6aeb0
Copy address: 0x55a3c2e6aeb0
[Destroy] Freeing buffer at address 0x55a3c2e6aeb0
[Destroy] Freeing buffer at address 0x55a3c2e6aeb0
free(): double free detected in tcache 2
Aborted (core dumped)
Watch Out: The Rule of Three
If your class needs a custom destructor (because it owns a raw resource), it almost certainly needs a custom copy constructor AND a custom copy assignment operator too. Defining only one of the three is a very common source of memory bugs. This pattern is so important it has a name: the Rule of Three.
Production Insight
In production, shallow copy errors are intermittent. They only trigger when objects get copied — which depends on code paths you don't control.
Use AddressSanitizer in CI. It catches double-free on every run, not just when the stars align.
Rule: if your class has a destructor, write all three. No shortcuts.
Key Takeaway
The compiler generates a shallow copy.
Shallow copies with raw pointers cause double-free crashes.
Always check: does my class manage a resource? If yes, write the full Rule of Three.

Writing a Deep Copy Constructor That Actually Works

A deep copy constructor solves the shared-pointer problem by allocating brand-new memory for the copy and then copying the data across — not just the address. The result is two completely independent objects that happen to hold identical data at that moment in time.

The signature of a copy constructor is always the same pattern: ClassName(const ClassName& source). The parameter is a const reference to the same type. It must be a reference — if it were passed by value, C++ would need to copy it, which would call the copy constructor again, causing infinite recursion. The const is there because copying shouldn't modify the source.

Let's fix our NaiveBuffer by renaming it DocumentBuffer and adding a proper copy constructor, copy assignment operator, and destructor — the full Rule of Three in action.

DocumentBuffer.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <cstring>
#include <algorithm>

namespace io::thecodeforge::core {

class DocumentBuffer {
public:
    char* content;   
    int   capacity;  

    explicit DocumentBuffer(const char* text) {
        capacity = static_cast<int>(std::strlen(text)) + 1;
        content  = new char[capacity];
        std::strcpy(content, text);
        std::cout << "[Constructor] Created at " << (void*)content << "\n";
    }

    // --- The Deep Copy Constructor ---
    DocumentBuffer(const DocumentBuffer& source) {
        capacity = source.capacity;
        content  = new char[capacity];          // Allocate FRESH memory
        std::strcpy(content, source.content);   // Copy actual DATA
        std::cout << "[Copy Constructor] Cloned to " << (void*)content << "\n";
    }

    // --- The Deep Copy Assignment Operator ---
    DocumentBuffer& operator=(const DocumentBuffer& source) {
        if (this == &source) return *this;      // Self-assignment guard

        delete[] content;                       // Cleanup existing resource
        capacity = source.capacity;
        content  = new char[capacity];
        std::strcpy(content, source.content);
        std::cout << "[Copy Assignment] Re-assigned at " << (void*)content << "\n";
        return *this;
    }

    ~DocumentBuffer() {
        std::cout << "[Destructor] Freeing " << (void*)content << "\n";
        delete[] content;  
    }
};

}

int main() {
    using namespace io::thecodeforge::core;
    DocumentBuffer draft("TheCodeForge");
    DocumentBuffer backup = draft; // Copy Constructor
    return 0;
}
Output
[Constructor] Created at 0x55f1a3c02eb0
[Copy Constructor] Cloned to 0x55f1a3c02ed0
[Destructor] Freeing 0x55f1a3c02ed0
[Destructor] Freeing 0x55f1a3c02eb0
Pro Tip: Use std::string and Smart Pointers to Sidestep This Entirely
In modern C++ (C++11 and beyond), wrapping your resource in a std::string, std::vector, or std::unique_ptr means the compiler-generated copy constructor does the right thing automatically — because those types already implement deep copying. Prefer this to managing raw pointers yourself unless you're writing low-level infrastructure code.
Production Insight
The allocation inside a deep copy can fail if the heap is exhausted. A std::bad_alloc exception in a constructor leaves the object partially built.
Wrap your copy constructor logic in a try-catch or use nothrow new if you must keep the class noexcept.
Rule: deep copy constructors should either guarantee success via std::nothrow or throw and let the caller handle it.
Key Takeaway
Deep copy = allocate new memory + copy data.
Two independent objects = safe destruction in any order.
Use C++ standard library types to avoid writing this code at all.

When to Delete the Copy Constructor — and the Move Constructor Alternative

Sometimes copying an object makes no logical sense. A database connection, a file handle, or a mutex represent unique real-world resources that can't meaningfully be duplicated. If you copy a database connection object, should both copies now own the same connection? The safest answer is to make copying impossible by deleting the copy constructor.

C++11 introduced the move constructor as a complementary tool. Where a copy says 'make me an identical twin', a move says 'transfer ownership to me — the original gives up its resource'. Moves are typically $O(1)$ operations because they just swap pointers, whereas deep copies are $O(N)$.

DatabaseConnection.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <string>
#include <utility>

namespace io::thecodeforge::db {

class DatabaseConnection {
private:
    std::string* conn_string;

public:
    explicit DatabaseConnection(const std::string& str) {
        conn_string = new std::string(str);
        std::cout << "[DB] Connection opened.\n";
    }

    // DISABLE COPYING
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;

    // ENABLE MOVING (Ownership Transfer)
    DatabaseConnection(DatabaseConnection&& other) noexcept : conn_string(other.conn_string) {
        other.conn_string = nullptr; // Leave donor in valid but empty state
        std::cout << "[DB] Connection moved.\n";
    }

    ~DatabaseConnection() {
        if (conn_string) {
            delete conn_string;
            std::cout << "[DB] Connection closed.\n";
        }
    }
};

}

int main() {
    using namespace io::thecodeforge::db;
    DatabaseConnection conn("sql://prod:5432");
    // DatabaseConnection copy = conn; // This would cause COMPILE ERROR
    DatabaseConnection moved_conn = std::move(conn); // This works!
    return 0;
}
Output
[DB] Connection opened.
[DB] Connection moved.
[DB] Connection closed.
Interview Gold: Copy vs Move Semantics
Interviewers love asking the difference between a copy constructor and a move constructor. The crisp answer: a copy constructor creates an independent duplicate (both objects remain valid); a move constructor transfers ownership (the source is left in a valid-but-empty state). Moves are typically O(1); copies are typically O(n) where n is the size of the data.
Production Insight
Deleting the copy constructor prevents accidental copies, but you must still handle situations where C++ tries to copy (e.g., passing by value). at compile time.
If a move constructor deletes, some std algorithms may fall back to copy and fail to compile. Mark move operations noexcept for better performance in containers.
Rule: delete copy for unique resources; provide a noexcept move constructor.
Key Takeaway
Delete copy constructor when duplication doesn't make sense.
Move constructor transfers ownership with O(1) work.
Always make move operations noexcept for optimal container reallocation.

When the Copy Constructor Gets Called — and the Traps You'll Hit

The copy constructor isn't just for explicit =. It's triggered in three common scenarios that often surprise junior engineers:

  1. Pass by value: void func(MyClass obj) — every call copies the object.
  2. Return by value: MyClass create() { MyClass tmp; return tmp; } — the return value is copied unless copy elision kicks in.
  3. Container insertion: std::vector<MyClass> vec; vec.push_back(obj); — stores a copy.

But here's where the traps lie: if you pass a temporary (rvalue) to a function that expects a parameter by value, the compiler may use the move constructor instead if available. If your class has only a copy constructor and not a move constructor, the copy constructor will be called even for temporaries — which is a performance hit. Also, brace initialisation (MyClass obj = {arg}) might call the copy constructor if the constructor is explicit.

Understanding when copying happens is critical for performance-critical code. Every unnecessary copy in a hot path adds O(n) time and memory pressure.

CopyTrigger.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <vector>

namespace io::thecodeforge::examples {

class Tracker {
public:
    int id;
    Tracker(int i) : id(i) { std::cout << "Construct " << id << "\n"; }
    Tracker(const Tracker& s) : id(s.id) { std::cout << "Copy " << id << "\n"; }
    Tracker(Tracker&& s) noexcept : id(s.id) { s.id = -1; std::cout << "Move " << id << "\n"; }
    ~Tracker() { std::cout << "Destroy " << id << "\n"; }
};

Tracker create() {
    Tracker local(1);
    return local; // Moves or elides
}

void consume(Tracker t) { /* pass by value */ }
}

int main() {
    using namespace io::thecodeforge::examples;
    Tracker a(2);
    Tracker b = a;       // Copy
    Tracker c = create(); // Move or elision
    consume( create() );  // Move into parameter
    std::vector<Tracker> v;
    v.push_back(a);       // Copy into vector
    return 0;
}
Output
Construct 2
Copy 2
Construct 1
Move 1
Destroy 1
Construct 1
Move 1
Destroy 1
Move 1
Destroy 1
Copy 2
Destroy 2
Destroy 2
Destroy 2
Performance Tip: Reduce Unnecessary Copies
In hot code, prefer passing by const reference (const MyClass&) instead of by value. For returning objects, rely on RVO by returning the object directly (not std::move on locals — that can inhibit elision). Use reserve() on vectors to avoid repeated copies during reallocation.
Production Insight
A common performance bug is passing large containers by value into every function. Each call copies the entire data.
We once saw a 300x slowdown because a logger was taking a std::string by value in a tight loop — every log line copied 200KB of payload.
Rule: if you don't intend to modify the input, pass by const&. Only copy when you truly need a local mutable copy.
Key Takeaway
Copy happens on pass-by-value, return-by-value, and container insertion.
Move constructors avoid deep copies for temporaries.
Measure with profiling before optimising — compiler elision often surprises.

Copy Elision and Return Value Optimization (RVO) — When the Copy Doesn't Happen

The C++ standard allows compilers to elide (skip) a copy or move constructor call under specific circumstances, even if the constructor has side effects. This is called copy elision. The most common form is Return Value Optimization (RVO): when a function returns a local variable, the compiler can construct it directly into the caller's variable, skipping the copy or move.

Since C++17, guaranteed copy elision is mandatory for certain cases: when a prvalue (pure rvalue) is used to initialise an object directly. That means MyClass obj = MyClass(42); constructs obj in place without calling the copy or move constructor — period.

Understanding elision is vital because you cannot rely on side effects in copy/move constructors for correctness. The compiler may or may not call them. RVO also affects performance: if you disable it (e.g., by returning std::move(local) instead of local), you may inhibit elision and force a move that's actually slower.

CopyElisionDemo.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

namespace io::thecodeforge::examples {

class Tracked {
public:
    Tracked() { std::cout << "default\n"; }
    Tracked(const Tracked&) { std::cout << "copy\n"; }
    Tracked(Tracked&&) noexcept { std::cout << "move\n"; }
    ~Tracked() { std::cout << "~destruct\n"; }
};

Tracked make() {
    Tracked local;
    return local; // RVO: may elide copy/move
}

}

int main() {
    using namespace io::thecodeforge::examples;
    Tracked obj = make(); // C++17: guaranteed elision
    Tracked x = Tracked(); // prvalue init: no copy
    return 0;
}
Output
default
~destruct
default
~destruct
C++17 Guaranteed Elision
Since C++17, the compiler must elide the copy when a prvalue is used to initialise an object of the same type. This means Tracked obj = Tracked(); does NOT call the copy or move constructor — even if they have side effects. This is a safety guarantee for factory functions and resource handles.
Production Insight
RVO doesn't apply when you return a function parameter or a member variable from a class. Those still require a copy or move.
If you use -fno-elide-constructors (GCC/Clang), you'll see the raw number of copies. Useful for debugging, but never rely on it in production.
Rule: write copy constructors that are correct even if never called. The compiler might elide them entirely.
Key Takeaway
Copy elision and RVO allow the compiler to skip copy/move constructors.
C++17 mandates elision for prvalue initialisation.
Never assume your copy constructor will be called — write it correctly regardless.

The Rule of Five: Expanding for Move Semantics

With C++11, the Rule of Three became the Rule of Five. If your class manages a resource, you should now consider defining:

  • Destructor
  • Copy constructor
  • Copy assignment operator
  • Move constructor
  • Move assignment operator

If you define a destructor, copy constructor, or copy assignment operator, the compiler will deprecate the default move operations (they are implicitly defined, but you should explicitly define them for correctness and performance). Similarly, if you define move operations, the copy operations are implicitly deleted.

In practice, if you're following the Rule of Five, use the copy-and-swap idiom for the copy assignment operator. It provides strong exception safety and reduces code duplication. The move constructor and assignment operator can simply swap pointers.

RuleOfFive.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>
#include <cstring>
#include <algorithm>

namespace io::thecodeforge::core {

class Resource {
    char* data_;
    size_t size_;
public:
    explicit Resource(const char* s) {
        size_ = std::strlen(s) + 1;
        data_ = new char[size_];
        std::strcpy(data_, s);
    }
    ~Resource() { delete[] data_; }

    // Copy
    Resource(const Resource& other) 
        : size_(other.size_), data_(new char[other.size_]) {
        std::strcpy(data_, other.data_);
    }
    Resource& operator=(Resource other) { // copy-and-swap
        swap(*this, other);
        return *this;
    }

    // Move
    Resource(Resource&& other) noexcept 
        : Resource() { // default constructor
        swap(*this, other);
    }
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    friend void swap(Resource& a, Resource& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }
};

}

int main() {
    using namespace io::thecodeforge::core;
    Resource r1("Hello");
    Resource r2 = r1;           // copy
    Resource r3 = std::move(r1); // move
    r2 = Resource("World");     // move assignment
    return 0;
}
Mental Model: The Rule of Five as a Contract
  • Each special member function defines one ownership operation: destroy, copy, move.
  • Missing one creates an implicit broken assumption (e.g., no move means copies for temporaries).
  • If you write none, the compiler handles it correctly only for types without raw resources.
  • The copy-and-swap idiom unifies copy assignment and provides strong exception safety.
Production Insight
Not defining move operations can kill performance in containers. std::vector reallocation copies all elements if only copy is available — O(n) per reallocation.
With noexcept move, vectors use memcpy-like optimisations.
Rule: if you write a destructor, define all five. Use = default where the compiler behaviour is correct.
Key Takeaway
Rule of Five = destructor + copy/move constructor + copy/move assignment.
Move operations should be noexcept for optimal container performance.
Copy-and-swap idiom provides strong exception safety and less code duplication.
● Production incidentPOST-MORTEMseverity: high

Double-Free Crash in Logging Pipeline After Code Refactor

Symptom
Crash with 'free(): double free detected in tcache 2' or 'Segmentation fault' on program exit. Only occurs when buffers are copied (e.g., passed by value to a logging function).
Assumption
The engineer assumed the compiler-generated copy constructor would handle everything correctly because it compiled without warnings.
Root cause
The buffer class held a raw char* pointer. The default copy constructor copied the pointer address, not the data. Both the original and the copy pointed to the same heap memory. When one was destroyed, it freed the memory; when the other was destroyed, it tried to free the same memory again.
Fix
Implement a deep copy constructor that allocates new memory and copies the data. Also add a copy assignment operator and a proper destructor — the full Rule of Three.
Key lesson
  • Never trust the compiler-generated copy constructor when your class manages a raw resource.
  • If you write a destructor, you almost certainly need a copy constructor and copy assignment operator.
  • Use AddressSanitizer (-fsanitize=address) during development to catch these bugs early.
Production debug guide3 entries
Symptom · 01
Double free or memory corruption on program exit. Only happens when objects are passed by value.
Fix
Run with AddressSanitizer (-fsanitize=address -g) to identify the exact allocation/free mismatch. Look for 'double-free' stack traces pointing to destructors.
Symptom · 02
Changes to one object unexpectedly affect another object that was supposedly a copy.
Fix
Check if the class has a custom copy constructor. If not, the default shallow copy is sharing memory. Add std::cout in the destructor printing the this pointer to confirm multiple objects are freeing the same address.
Symptom · 03
Segfault when accessing a copied object after the source object was destroyed.
Fix
The copy was shallow and the source's destructor freed the shared memory. Use valgrind or ASan to detect use-after-free. Ensure copy constructor and assignment operator are deep-copying.
★ Quick Debug Cheat Sheet: Common Copy Constructor FailuresUse these commands to diagnose shallow copy and double-free issues immediately.
Double free on exit
Immediate action
Enable AddressSanitizer and rebuild
Commands
g++ -fsanitize=address -g -o program program.cpp
./program 2>&1 | grep -A 5 'double-free'
Fix now
Implement a deep copy constructor and copy assignment operator, or delete them if copying is invalid.
Heap use-after-free when copying objects+
Immediate action
Run with valgrind
Commands
valgrind --tool=memcheck --leak-check=full ./program
Look for 'Invalid read' errors pointing to your destructor or copy constructor
Fix now
In the copy constructor, allocate new memory for the copy instead of copying the pointer.
Copy assignment operator not called, shallow copy occurs+
Immediate action
Check if the class has a custom operator= or relies on compiler default
Commands
grep -c 'operator=' source.cpp
Add a `std::cout` message inside any custom operator= to verify it's invoked
Fix now
Implement the copy assignment operator using the copy-and-swap idiom for strong exception safety.
Shallow Copy vs Deep Copy vs Move
AspectShallow Copy (Compiler Default)Deep Copy (Custom Constructor)Move Constructor
What gets copiedPointer address only (4 or 8 bytes)Pointer address + all data it points toPointer address; source pointer set to null
Memory ownershipBoth objects share the same heap blockEach object owns its own independent heap blockOnly the destination owns the block after move
Destructor safetyDouble-free crash when second object is destroyedEach destructor frees its own memory safelySource destructor no-ops since data pointer is null
Mutation independenceChanging data via one object affects the otherObjects are fully independent after copyingOnly the moved-to object holds data; source is empty
Performance costO(1) — just copies a few bytesO(n) — proportional to data sizeO(1) — pointer swap or null assignment
When it's correctOnly when class has no pointer/resource membersAny class that owns heap memory or external resourcesWhen the source is a temporary or about to be destroyed
Rule of Three/Five required?No (but dangerous if resources exist)Yes — always pair with destructor and copy assignmentYes — pair with destructor and copy operations if copying allowed

Key takeaways

1
The compiler-generated copy constructor performs a shallow (member-wise) copy
safe for value types, silently dangerous for any class that owns raw heap memory via a pointer.
2
A deep copy constructor allocates fresh memory and copies the data across; after the copy both objects are fully independent and can be safely destroyed in any order.
3
The Rule of Three
if your class needs any one of a custom destructor, copy constructor, or copy assignment operator, it almost certainly needs all three — missing one is a common source of double-free crashes and memory leaks.
4
Use = delete to make a class non-copyable when copying makes no logical sense (file handles, sockets, threads). Pair it with a move constructor to still allow efficient ownership transfer.
5
Copy elision and RVO can eliminate copy/move constructor calls entirely
never rely on side effects in those constructors for program logic.

Common mistakes to avoid

4 patterns
×

Forgetting the copy assignment operator after writing a copy constructor

Symptom
The class looks safe because the copy constructor does a deep copy, but buffer2 = buffer1; still performs a shallow copy via the compiler-generated assignment operator, causing a double-free when both go out of scope.
Fix
Whenever you write a copy constructor, immediately write the copy assignment operator too (and vice versa). The Rule of Three exists for exactly this reason.
×

Passing a copy constructor parameter by value instead of by reference

Symptom
Writing DocumentBuffer(DocumentBuffer source) instead of DocumentBuffer(const DocumentBuffer& source) causes infinite recursion: to pass source by value, C++ must copy it, which calls the copy constructor, which tries to copy source again, and so on until the stack overflows.
Fix
The parameter must always be const ClassName&. The compiler will actually error on this in many cases, but understanding why the rule exists matters.
×

Not handling self-assignment in the copy assignment operator

Symptom
Writing archive = archive; is legal C++ and must work correctly. Without a self-assignment guard (if (this == &source) return *this;), the operator frees the old memory with delete[] content first, then tries to copy from source.content — which is now dangling memory that was just freed.
Fix
Always put the if (this == &source) return *this; guard at the very top of your copy assignment operator before touching any memory.
×

Assuming the compiler-generated copy constructor is safe when class uses smart pointers

Symptom
Smart pointers like std::unique_ptr enforce unique ownership, but the compiler-generated copy constructor tries to copy them — which is deleted. The code won't compile. With std::shared_ptr, the copy is shallow (reference count increase) which may not be intended.
Fix
For std::unique_ptr, explicitly delete or implement a custom copy (deep copy) if needed. For std::shared_ptr, understand that copies share the same managed object — if you need a deep copy, you must implement it manually.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the 'Copy-and-Swap' idiom. How does it simplify the implementati...
Q02SENIOR
What is the difference between a Copy Constructor and a Copy Assignment ...
Q03SENIOR
What is the Rule of Five? Why did the introduction of C++11 expand the R...
Q04JUNIOR
Why must the copy constructor's parameter be passed by reference? Walk t...
Q05SENIOR
Explain 'Copy Elision' and 'Return Value Optimization' (RVO). Why might ...
Q01 of 05SENIOR

Explain the 'Copy-and-Swap' idiom. How does it simplify the implementation of the copy assignment operator while providing strong exception safety?

ANSWER
Copy-and-swap is a technique where the copy assignment operator takes its parameter by value (invoking the copy constructor), then swaps the contents of this with the parameter. This provides strong exception safety because if the copy construction throws, this remains unchanged. After the swap, the destructor of the parameter cleans up the old resources. It also handles self-assignment correctly without an explicit guard.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What happens if I write a Copy Constructor but forget the Copy Assignment Operator?
02
When is the copy constructor called automatically in C++?
03
What is the difference between a copy constructor and a copy assignment operator?
04
Why must the copy constructor take its parameter by reference and not by value?
05
What is the 'Rule of Zero' in Modern C++?
🔥

That's C++ Basics. Mark it forged?

5 min read · try the examples if you haven't

Previous
Virtual Functions in C++
17 / 19 · C++ Basics
Next
Static Members in C++