Copy Constructor in C++: Default, User-Defined, Deep Copy & Best Practices

A Copy Constructor in C++ is a special constructor that creates a new object as a copy of an existing one, typically declared as ClassName(const ClassName& other). It is invoked on copy-initialization, when passing or returning objects by value, and in several other copying scenarios.

This beginner-friendly guide explains the Copy Constructor in C++ with commented examples, outputs, best practices, and “Try it yourself” challenges after each section so first-time learners can follow along with confidence.

🔹 What is a Copy Constructor in C++?

The copy constructor initializes a new object from an existing object of the same type. Canonical form:

// Canonical signature
ClassName(const ClassName& other);

When it’s called:

  • Copy-initialization: T b = a;
  • Pass-by-value and return-by-value (except when copy elision is applied)
  • Explicit copy: T c(a);
  • Throwing/catching exceptions by value
#include <iostream>
using namespace std;
struct Point {
    int x{0}, y{0};
    // User-defined copy constructor with logging
    Point(const Point& other) : x(other.x), y(other.y) {
        cout << "Copy ctor: (" << other.x 
        << "," << other.y << ")\n";
    }
    Point() = default;
    Point(int x, int y) : x(x), y(y) {}
};
void byValue(Point p) {
    // copy constructor may be called here
    cout << "Inside byValue: (" << p.x 
    << "," << p.y << ")\n";
}
Point makePoint() {
    Point p(7, 8);
    return p; // may elide copy (see copy elision section)
}
int main() {
    Point a(1, 2);
    Point b = a; // copy-initialization (calls copy ctor)
    Point c(a);  // direct-initialization (calls copy ctor)
    byValue(a);  // copy into parameter (may call copy ctor)
    Point d = makePoint(); // copy elision likely
}

Output

Copy ctor: (1,2)
Copy ctor: (1,2)
Copy ctor: (1,2)

Note: The exact number of copy constructor calls can differ due to compiler optimizations (copy elision).

Try it yourself

  • Add a message in the default constructor and observe the order of calls.
  • Comment out the user-defined copy constructor and see how the compiler-generated copy behaves.

🔹 Default vs User-Defined Copy Constructor

If you don’t declare one, the compiler generates a memberwise (field-by-field) copy constructor. That is fine for types that don’t own resources. For resource-owners (like raw pointers, file handles), you often need a deep copy to avoid double-free or aliasing bugs.

// Memberwise copy is OK for value-like types
struct Pod {
    int id;
    double score;
    // Default copy ctor performs memberwise copy: safe here
};
// Memberwise copy is dangerous for owning raw pointers (shallow copy)
struct ShallowBuffer {
    size_t n{0};
    int* data{nullptr};
    ShallowBuffer(size_t n) : n(n), data(new int[n]{}) {}
    // Compiler-generated copy ctor would copy the pointer (shallow),
    // leading to two objects owning the same memory (double delete!)
    ~ShallowBuffer() { delete[] data; }
};

For ShallowBuffer, you must write a deep copy constructor that allocates new memory and copies elements. See next section.

Try it yourself

  • Create a Pod object and copy it. Print the fields to confirm values match.
  • Attempt copying ShallowBuffer without a user-defined copy constructor (don’t run delete twice). Print the two data addresses to see they alias—then fix with a deep copy.

🔹 Implementing a Deep Copy (Rule of Three)

Resource-owning classes must define a correct copy constructor, copy assignment, and destructor (Rule of Three). Below, the copy constructor performs a deep copy so each object owns its own buffer safely.

#include <cstddef>
#include <algorithm>
#include <iostream>
using namespace std;
class Buffer {
    size_t n{0};
    int* data{nullptr};
public:
    // ctor
    explicit Buffer(size_t n) : n(n), data(new int[n]{}) {}
    // deep-copy constructor
    Buffer(const Buffer& other) : n(other.n), data(new int[other.n]) {
        std::copy(other.data, other.data + n, data);
        cout << "Buffer copy ctor: deep-copied " 
        << n << " ints\n";
    }
    // copy assignment
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            int* newData = new int[other.n];
            std::copy(other.data, other.data + other.n, newData);
            delete[] data;
            data = newData;
            n = other.n;
        }
        return *this;
    }
    // destructor
    ~Buffer() { delete[] data; }
    // simple access
    void set(size_t i, int v) { data[i] = v; }
    int get(size_t i) const { return data[i]; }
    size_t size() const { return n; }
};
int main() {
    Buffer a(3);
    a.set(0, 10);
    a.set(1, 20);
    a.set(2, 30);
    Buffer b = a; // deep copy
    b.set(1, 999);
    cout << a.get(1) << " (should be 20)\n";
    cout << b.get(1) << " (should be 999)\n";
}

Output

Buffer copy ctor: deep-copied 3 ints
20 (should be 20)
999 (should be 999)

Each object owns its own memory, avoiding aliasing and double deletion. This is the core reason to customize the Copy Constructor in C++ for resource-owning classes.

Try it yourself

  • Add a move constructor and move assignment to follow the Rule of Five for performance.
  • Switch int* to std::unique_ptr<int[]> and observe how the Rule of Zero simplifies your class (copy disabled by default, move-friendly).

🔹 When Is the Copy Constructor Invoked?

Typical triggers for a Copy Constructor in C++ (subject to copy elision):

  • Copy-initialization: T b = a;
  • Direct-initialization: T b(a);
  • Pass-by-value parameters
  • Return-by-value (may be elided)
  • Throwing/catching exceptions by value
  • Initializing elements inside containers from existing objects

You can instrument your copy constructor with logs (as shown earlier) to see which paths trigger it in your build with your optimization settings.

🔹 Copy Constructor vs Copy Assignment vs Move

They are distinct operations:

  • Copy constructor: creates a new object from an existing one.
  • Copy assignment: replaces the contents of an existing object.
  • Move constructor/assignment: transfer resources from temporaries to avoid copies.
#include <iostream>
#include <utility>
using namespace std;
struct Tracer {
    Tracer() 
    { cout << "default ctor\n"; }
    Tracer(const Tracer&) 
    { cout << "copy ctor\n"; }
    Tracer& operator=(const Tracer&) 
    { cout << "copy assign\n"; return *this; }
    Tracer(Tracer&&) noexcept 
    { cout << "move ctor\n"; }
    Tracer& operator=(Tracer&&) noexcept 
    { cout << "move assign\n"; return *this; }
};
Tracer make() { return Tracer(); }
int main() {
    Tracer a; // default
    Tracer b = a; // copy ctor
    Tracer c; c = a; // copy assign
    Tracer d = make(); // move or elide
    Tracer e; e = make(); // move assign or elide then move
}

Output

default ctor
copy ctor
default ctor
copy assign
default ctor
move assign

Exact output depends on copy elision and optimization. The key is understanding which operation is expected in each context.

🔹 Copy Elision and RVO (C++17+)

Modern compilers often avoid creating temporary copies through copy elision. Since C++17, in some cases (like returning a prvalue), copy/move construction is guaranteed to be elided, so your copy constructor is not called even if it exists.

// With C++17 guaranteed elision for prvalues, this may call no copy/move ctor
#include <iostream>
struct X {
    X() { std::cout << "X()\n"; }
    X(const X&) { std::cout << "X copy\n"; }
};
// no copy due to guaranteed elision (prvalue)
X makeX() { return X(); } 
int main() {
    X x = makeX(); // likely prints only "X()"
}

Don’t rely on copy-constructor side effects for logic. They may be skipped by elision.

🔹 Disabling Copy (Non-Copyable Types)

Some types should not be copyable (e.g., managing unique resources). You can explicitly delete the Copy Constructor in C++ and copy assignment operator.

struct UniqueHandle {
    int fd{-1};
    UniqueHandle() = default;
    // forbid copying
    UniqueHandle(const UniqueHandle&) = delete;
    UniqueHandle& operator=(const UniqueHandle&) = delete;
    // allow moving
    UniqueHandle(UniqueHandle&& other) noexcept : 
    fd(other.fd) { other.fd = -1; }
    UniqueHandle& operator=(UniqueHandle&& other) noexcept {
        if (this != &other) {
            /* close fd if needed, then transfer */
            fd = other.fd;
            other.fd = -1;
        }
        return *this;
    }
};

This pattern prevents accidental copies that would duplicate ownership of a single resource.

🔹 Rule of Three/Five/Zero in Practice

  • Rule of Three: If you define any of copy constructor, copy assignment, or destructor, you likely need all three.
  • Rule of Five: In C++11+, also consider move constructor and move assignment.
  • Rule of Zero: Prefer RAII members (like std::vector, std::string, smart pointers) so you need none; the compiler generates correct special members.

Favor Rule of Zero when possible—let well-designed members manage copying and moving for you.

🔹 Mini Project: Safe Image Class (Deep Copy vs Rule of Zero)

Below are two approaches to a simple Image type. The raw-pointer version requires a deep Copy Constructor in C++; the std::vector-based version benefits from Rule of Zero and needs no custom copy logic.

// Version A: manual resource management (needs deep copy)
#include <algorithm>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
class ImageA {
    size_t w{0}, h{0};
    unsigned char* pixels{nullptr};
public:
    ImageA(size_t w, size_t h) : w(w), h(h), 
    pixels(new unsigned char[w*h]{}) {}
    ~ImageA() { delete[] pixels; }
    // deep copy constructor
    ImageA(const ImageA& other) : w(other.w), 
    h(other.h), pixels(new unsigned char[w*h]) {
        std::memcpy(pixels, other.pixels, w*h);
        cout << "ImageA copied " << 
        (w*h) << " bytes\n";
    }
    // copy assignment omitted for brevity (should also deep copy)
    void set(size_t idx, unsigned char v) 
    { pixels[idx] = v; }
    unsigned char get(size_t idx) const 
    { return pixels[idx]; }
};
// Version B: Rule of Zero with std::vector (no custom copy ctor needed)
class ImageB {
    size_t w{0}, h{0};
    // copies/moves handled by vector
    std::vector<unsigned char> pixels; 
public:
    ImageB(size_t w, size_t h) : w(w), h(h), pixels(w*h, 0) {}
    void set(size_t idx, unsigned char v) 
    { pixels[idx] = v; }
    unsigned char get(size_t idx) const 
    { return pixels[idx]; }
};
int main() {
    ImageA a(4, 1);
    a.set(0, 123);
    ImageA b = a; // deep copy via user-defined copy ctor
    b.set(0, 99);
    cout << "ImageA a=" << (int)a.get(0) 
    << ", b=" << (int)b.get(0) << "\n";
    ImageB x(4, 1);
    x.set(0, 42);
    ImageB y = x; // vector handles copy safely (Rule of Zero)
    y.set(0, 7);
    cout << "ImageB x=" << (int)x.get(0) 
    << ", y=" << (int)y.get(0) << "\n";
}

Output

ImageA copied 4 bytes
ImageA a=123, b=99
ImageB x=42, y=7

Rule of Zero is simpler and safer. Use manual deep copies only when necessary.

Try it yourself

  • Add a proper copy assignment to ImageA using copy-and-swap.
  • Time copying ImageA vs ImageB for large buffers to see the benefit of standard containers.

🔹 Best Practices and Common Pitfalls

  • Prefer Rule of Zero: use standard containers and smart pointers to avoid writing a custom Copy Constructor in C++.
  • If you own a raw resource, implement deep copy (Rule of Three/Five) or disable copy and allow move only.
  • Mark the copy constructor noexcept only if it truly cannot throw (rare for deep copies).
  • Avoid performing expensive work in copy constructors if moving is possible (implement move operations).
  • Beware of self-assignment in copy assignment; copy constructors don’t need to handle self-assignment.
  • Don’t rely on copy constructor side-effects; copy elision may bypass them.

Try it yourself

  • Convert a raw-pointer-owning class to use std::unique_ptr or std::vector and delete custom copy logic.
  • Write unit tests to ensure copying creates independent objects (changes in one don’t affect the other).

🔹 FAQs about Copy Constructor in C++

Q1: What’s the exact signature I should use?
The canonical form is ClassName(const ClassName& other). The parameter must be a const reference to avoid infinite recursion and allow copying const objects.

Q2: When does the compiler generate a copy constructor?
If you don’t declare one and copying is viable for all members, the compiler synthesizes a memberwise copy constructor.

Q3: Copy constructor vs copy assignment—what’s the difference?
Copy constructor creates a new object from an existing one. Copy assignment replaces the contents of an existing object. They are triggered in different contexts and both may need deep copy handling.

Q4: Do I need a copy constructor if I use std::vector and std::string?
Usually no. Follow the Rule of Zero and let these members handle copying and moving safely.

Q5: How does copy elision affect the copy constructor?
Compilers may elide copies (especially in C++17 return-by-value scenarios). Your copy constructor might not be invoked even if it exists.

Q6: Can I make a class non-copyable?
Yes. ClassName(const ClassName&) = delete; and operator=(const ClassName&) = delete;. Often combine with move operations.

Q7: Should copy constructors throw exceptions?
They can, but be careful. Exceptions during copying should leave the program in a safe state. Prefer strong exception safety (copy into a temp, then commit).

Q8: What about copying polymorphic objects?
Copying via base-class interfaces is tricky. Provide a virtual clone() that returns std::unique_ptr<Base> to copy the dynamic type correctly.

Q9: Does passing by const reference avoid copies?
Yes. Prefer const T& (or forwarding references) in parameters to avoid unnecessary copying when you don’t need ownership.

Q10: Is copy-and-swap still recommended?
Yes, it’s a clean way to implement copy assignment (not the copy constructor) with strong exception safety. It pairs well with the Rule of Five.

Try it yourself

  • Add a clone() to a polymorphic base and override it in derived classes. Verify correct deep copies via unique_ptr.
  • Rework a copy assignment using copy-and-swap and test exception safety by injecting a throwing allocator.

🔹 Wrapping Up

The Copy Constructor in C++ initializes a new object from an existing one and is central to correct, safe copying. Use Rule of Zero when possible, write deep copies for resource owners (Rule of Three/Five), understand when copies are invoked, and be mindful of copy elision. Practice the “Try it yourself” tasks to internalize these patterns and write robust, maintainable C++ code.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.