Question
The Rule of Three in C++: Copying Objects, Copy Constructor, and Copy Assignment
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 ageis 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.
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.datapoints to that memory.a.sizebecomes2.
2. a.set(0, 5);
- The first element of
a's array becomes5.
3. a.set(1, 9);
- The second element of
a's array becomes9.
At this point, a owns its own array:
a.data -> [5, 9]
4. Buffer b = a;
- This creates a new object
bfroma. - The copy constructor runs.
- A new array of size 2 is allocated for
b. - The values
5and9are copied intob'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
bis destroyed, and its destructor deletesb.dataais destroyed, and its destructor deletesa.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.
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::stringstd::vectorstd::arraystd::unique_ptrstd::shared_ptrwhen 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::vectororstd::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
| Concept | When it happens | What it does | Typical syntax |
|---|---|---|---|
| Copy constructor | When creating a new object from another object | Builds a new object as a copy | MyClass b = a; |
| Copy assignment operator | When assigning to an existing object | Replaces existing state | b = a; |
| Destructor | When an object is destroyed | Releases owned resources | automatic at scope end |
Shallow copy vs deep copy
| Type of copy | What gets copied | Safe for raw owning pointers? | Result |
|---|---|---|---|
| Shallow copy | Member values only, including pointer addresses | No | Multiple objects may point to same resource |
| Deep copy | Underlying resource is duplicated | Yes | Each object owns its own resource |
Defaulted vs custom vs deleted copy
| Approach | Use when | Example |
|---|---|---|
| Compiler-generated / defaulted | Members already manage themselves safely | classes with std::string, std::vector |
| Custom copy operations | Class owns raw resources and needs deep copy | custom buffer class |
| Deleted copy operations | Object should not be copied | mutex wrapper, file handle owner |
Rule of Three vs Rule of Five
| Rule | Meaning |
|---|---|
| Rule of Three | If you define destructor, copy constructor, or copy assignment, you likely need all three |
| Rule of Five | In modern C++, move constructor and move assignment are also important when managing resources |
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,boolstd::stringstd::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.
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.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.