Question

The Rule of Three in C++: Copying Objects, Copy Constructor, and Copy Assignment

cppcopy-constructorassignment-operatorc++-faqrule-of-three

Question

In C++, what does it mean to copy an object?

What are the copy constructor and the copy assignment operator, and how are they different?

When do I need to declare or define these special member functions myself?

Also, how can I prevent objects of a class from being copied at all?

Short Answer

By the end of this page, you will understand what object copying means in C++, how the copy constructor and copy assignment operator work, why the Rule of Three exists, when you must define these functions yourself, and how to disable copying for classes that should not be duplicated.

Concept

In C++, copying an object means creating or updating one object so it has the same state as another object.

There are two main copy operations:

  • Copy constructor: creates a new object from an existing object
  • Copy assignment operator: replaces the contents of an existing object with another object's contents

For example:

MyClass a;
MyClass b = a;   // copy constructor

MyClass c;
c = a;           // copy assignment operator

At first, this may seem simple. But copying becomes important when a class manages a resource such as:

  • dynamically allocated memory
  • file handles
  • sockets
  • mutexes
  • database connections

If your class only contains simple values like int, double, or std::string, the compiler-generated copy behavior is usually correct.

But if your class owns a raw resource, a default copy can be dangerous. The compiler may perform a member-wise copy, which means it copies each field as-is. If one of those fields is a raw pointer, both objects may end up pointing to the same memory. That can cause:

  • double deletion
  • memory corruption
  • unexpected shared state

This is where the Rule of Three comes from:

If a class needs a custom destructor, it usually also needs a custom copy constructor and copy assignment operator.

Why? Because if you manually manage a resource, you must define how that resource is:

  • cleaned up
  • copied during construction
  • copied during assignment

A classic example is a class that owns a dynamically allocated array.

class Buffer {
private:
    int* data;
    std::size_t size;

public:
    Buffer(std::size_t s) : data(new int[s]), size(s) {}

    ~Buffer() {
        delete[] data;
    }
};

This class has a custom destructor, so it is already managing memory manually. If copied with the compiler-generated copy operations, only the pointer value gets copied, not the array itself. Then two objects try to delete the same memory.

So the Rule of Three says this class should probably define all three:

  • destructor
  • copy constructor
  • copy assignment operator

In modern C++, this idea is often extended to the Rule of Five and often avoided by using RAII types such as std::vector, std::string, and smart pointers.

Mental Model

Think of an object as a person carrying a backpack.

  • A normal value field like int age is like writing the age on a card. Copying it is easy.
  • A raw pointer is like carrying a note with a storage locker number. Copying the note does not copy the locker contents.

Now imagine two people both hold a note to the same locker. If both think they own it and both try to empty and destroy it, problems happen.

That is what shallow copying does with raw pointers.

  • Copy constructor = building a brand-new person with the same belongings
  • Copy assignment = taking an existing person and replacing their belongings with someone else's
  • Destructor = cleaning up what the person owns when they leave

The Rule of Three means: if your object owns a locker key, you must carefully define how the locker is copied, reassigned, and cleaned up.

Take Quiz

Syntax and Examples

Core syntax

Copy constructor

ClassName(const ClassName& other);

Copy assignment operator

ClassName& operator=(const ClassName& other);

Prevent copying

ClassName(const ClassName&) = delete;
ClassName& operator=(const ClassName&) = delete;

Example: safe deep copy

#include <iostream>
#include <cstddef>

class Buffer {
private:
    int* data;
    std::size_t size;

public:
    Buffer(std::size_t s) : data(new int[s]), size(s) {
        for (std::size_t i = 0; i < size; ++i) {
            data[i] = 0;
        }
    }

    // Copy constructor
    Buffer(const Buffer& other) : data(new int[other.size]), size(other.size) {
        for (std::size_t i = 0; i < size; ++i) {
            data[i] = other.data[i];
        }
    }

    // Copy assignment operator
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            int* newData = new int[other.size];
            for (std::size_t i = 0; i < other.size; ++i) {
                newData[i] = other.data[i];
            }

            delete[] data;
            data = newData;
            size = other.size;
        }
        return *this;
    }

    ~Buffer() {
        delete[] data;
    }

    void set(std::size_t index, int value) {
        if (index < size) {
            data[index] = value;
        }
    }

    void print() const {
        for (std::size_t i = 0; i < size; ++i) {
            std::cout << data[i] << ' ';
        }
        std::cout << '\n';
    }
};

int main() {
    Buffer a(3);
    a.set(0, 10);
    a.set(1, 20);
    a.set(2, 30);

    Buffer b = a;   // copy constructor
    Buffer c(3);
    c = a;          // copy assignment

    b.print();
    c.print();
}

Why this works

This class performs a deep copy:

  • each object gets its own separate array
  • destroying one object does not affect the other
  • changing one object does not change the other

If you removed the custom copy constructor and assignment operator, the pointer would be copied directly, which would be unsafe.

Step by Step Execution

Consider this example:

Buffer a(2);
a.set(0, 5);
a.set(1, 9);

Buffer b = a;

Here is what happens step by step.

1. Buffer a(2);

  • The constructor runs.
  • Memory for 2 integers is allocated with new int[2].
  • a.data points to that memory.
  • a.size becomes 2.

2. a.set(0, 5);

  • The first element of a's array becomes 5.

3. a.set(1, 9);

  • The second element of a's array becomes 9.

At this point, a owns its own array:

a.data -> [5, 9]

4. Buffer b = a;

  • This creates a new object b from a.
  • The copy constructor runs.
  • A new array of size 2 is allocated for b.
  • The values 5 and 9 are copied into b's array.

Now memory looks like this:

a.data -> [5, 9]
b.data -> [5, 9]

The values are equal, but the arrays are different memory blocks.

5. Program ends

  • b is destroyed, and its destructor deletes b.data
  • a is destroyed, and its destructor deletes a.data

Because a and b own different arrays, both deletions are safe.

What would go wrong with shallow copy?

If the compiler copied only the pointer value, memory would look like this instead:

a.data ----
          -> [5, 9]
b.data ----

Now both objects point to the same array. When both destructors call delete[], the same memory is deleted twice. That is undefined behavior.

Real World Use Cases

The Rule of Three matters when a class directly owns a resource.

Common examples

Managing dynamic memory

class ImageBuffer {
    unsigned char* pixels;
    int width;
    int height;
};

If pixels is allocated manually, copying must be handled carefully.

Wrapping file handles

A class may open a file in its constructor and close it in its destructor. Copying such an object may not make sense unless ownership rules are clearly defined.

Socket or network resource wrappers

Two objects should usually not both believe they uniquely own the same socket.

Custom container classes

If you build your own vector-like or string-like class using raw pointers, copying must create separate storage.

Device or system resource managers

Some resources cannot be duplicated safely at all, so copying should be disabled.

When this matters less

If your class is made of safe standard library types, copying is usually already handled correctly.

class Person {
private:
    std::string name;
    int age;
};

Here, std::string already manages its own memory correctly, so you usually do not need custom copy code.

Take Quiz

Real Codebase Usage

In real C++ codebases, developers usually try to avoid manual memory management where possible.

Common patterns

Prefer RAII types

Instead of owning raw pointers directly, use:

  • std::string
  • std::vector
  • std::array
  • std::unique_ptr
  • std::shared_ptr when shared ownership is truly needed

These types already define correct cleanup and copying behavior.

Use = default when the compiler-generated version is correct

class Person {
public:
    Person() = default;
    Person(const Person&) = default;
    Person& operator=(const Person&) = default;
    ~Person() = default;
};

This makes your intent explicit.

Use = delete to disable copying

class DatabaseConnection {
public:
    DatabaseConnection() = default;
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
};

This is common for classes managing unique resources.

Self-assignment protection

Many hand-written copy assignment operators guard against this:

if (this != &other) {
    // copy safely
}

This avoids deleting your own data before copying from it.

Copy-and-swap style

A common pattern for copy assignment is creating a safe copy first, then swapping resources. This can simplify exception-safe code.

Practical advice

In modern projects, if you find yourself writing all three manually, ask:

  • Can I replace raw pointers with std::vector or std::string?
  • Should this class be movable instead of copyable?
  • Should copying be deleted entirely?

Common Mistakes

1. Confusing initialization with assignment

These are different:

MyClass b = a; // copy constructor
b = a;         // copy assignment operator

Beginners often think both lines do the same thing. They do not.

2. Forgetting the destructor when owning raw memory

Broken example:

class BadBuffer {
private:
    int* data;

public:
    BadBuffer() {
        data = new int[10];
    }
};

This leaks memory because nothing frees data.

3. Defining a destructor but not copy operations

Broken example:

class BadBuffer {
private:
    int* data;

public:
    BadBuffer() {
        data = new int[10];
    }

    ~BadBuffer() {
        delete[] data;
    }
};

This looks better, but copying is now dangerous because the default copy operations still shallow-copy the pointer.

4. Forgetting self-assignment handling

Broken example:

BadBuffer& operator=(const BadBuffer& other) {
    delete[] data;
    data = new int[10];
    for (int i = 0; i < 10; ++i) {
        data[i] = other.data[i];
    }
    return *this;
}

If other is the same object as *this, the code deletes the data and then tries to read from freed memory.

Fix it:

if (this != &other) {
    // safe copy
}

5. Copying raw pointers instead of pointed-to data

Broken example:

data = other.data;

That copies the address, not the array contents.

6. Preventing copying the old way without understanding it

Older C++ code sometimes declares copy operations privately and does not define them. Modern C++ should usually use = delete instead.

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};

Comparisons

ConceptWhen it happensWhat it doesTypical syntax
Copy constructorWhen creating a new object from another objectBuilds a new object as a copyMyClass b = a;
Copy assignment operatorWhen assigning to an existing objectReplaces existing stateb = a;
DestructorWhen an object is destroyedReleases owned resourcesautomatic at scope end

Shallow copy vs deep copy

Type of copyWhat gets copiedSafe for raw owning pointers?Result
Shallow copyMember values only, including pointer addressesNoMultiple objects may point to same resource
Deep copyUnderlying resource is duplicatedYesEach object owns its own resource

Defaulted vs custom vs deleted copy

ApproachUse whenExample
Compiler-generated / defaultedMembers already manage themselves safelyclasses with std::string, std::vector
Custom copy operationsClass owns raw resources and needs deep copycustom buffer class
Deleted copy operationsObject should not be copiedmutex wrapper, file handle owner

Rule of Three vs Rule of Five

RuleMeaning
Rule of ThreeIf you define destructor, copy constructor, or copy assignment, you likely need all three
Rule of FiveIn modern C++, move constructor and move assignment are also important when managing resources
Take Quiz

Cheat Sheet

Quick reference

Copy constructor

ClassName(const ClassName& other);

Used when creating a new object from an existing one.

ClassName b = a;
ClassName b(a);

Copy assignment operator

ClassName& operator=(const ClassName& other);

Used when assigning to an already existing object.

b = a;

Rule of Three

If your class defines one of these, it probably needs all three:

  • destructor
  • copy constructor
  • copy assignment operator

Prevent copying

ClassName(const ClassName&) = delete;
ClassName& operator=(const ClassName&) = delete;

Safe assignment checklist

  • check for self-assignment: if (this != &other)
  • allocate new resources before deleting old ones when possible
  • return *this

Warning signs

You may need custom copy behavior if your class contains:

  • raw pointers that own memory
  • file handles
  • sockets
  • OS or hardware resources

Usually no custom copy needed

If your class only stores:

  • int, double, bool
  • std::string
  • std::vector
  • other well-behaved value types

then compiler-generated copy behavior is often enough.

FAQ

What is the Rule of Three in C++?

It says that if a class needs a custom destructor, it usually also needs a custom copy constructor and copy assignment operator.

What does copying an object mean in C++?

It means making one object take the same state as another, either by constructing a new object from it or assigning to an existing one.

What is the difference between copy constructor and copy assignment?

The copy constructor creates a new object from another object. The copy assignment operator copies into an already existing object.

When do I need to write a copy constructor myself?

Usually when your class directly owns a resource such as dynamically allocated memory or another unique resource.

How do I stop a class from being copied?

Delete the copy constructor and copy assignment operator:

MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;

Is the Rule of Three still relevant in modern C++?

Yes, but modern C++ often avoids manual resource management by using standard library types. You may also hear the Rule of Five.

Does std::string require the Rule of Three?

No. std::string already manages its own memory correctly, so classes containing it usually do not need custom copy operations.

What is a shallow copy in C++?

A shallow copy copies member values directly. For raw pointers, that means copying the address rather than duplicating the pointed-to data.

Take Quiz

Mini Project

Description

Build a small C++ class that manages a dynamically allocated character buffer. This project demonstrates why the Rule of Three exists by implementing safe copying for a class that owns raw memory.

Goal

Create a class that owns a heap-allocated string buffer, supports deep copying, and avoids double deletion.

Requirements

  • Create a class that stores text in dynamically allocated memory.
  • Implement a destructor to release the memory.
  • Implement a copy constructor that performs a deep copy.
  • Implement a copy assignment operator that safely copies data.
  • Show in main() that modifying one copy does not change another copy.
Take Quiz

Keep learning

Related questions

View more