Copy Semantics
Understand exactly what happens when a C++ object is copied — member by member, shallow versus deep — and why classes that own resources need to control that process.
Copy semantics describes what happens when one object's value is used to initialize or assign another object of the same type: which data gets duplicated, how, and what that means for objects that own resources such as dynamically allocated memory. C++ gives every class copy behavior by default, but that default is only correct for some classes — understanding exactly what a copy constructor and copy assignment operator do is essential for writing classes that behave correctly when copied.
What Is Copy Semantics?
Copy semantics is the set of rules governing what happens when one object's value is used to bring another object of the same type into existence, or to overwrite an already-existing object. Two special member functions are responsible for this: the copy constructor and the copy assignment operator.
Creating b from a invokes copying behavior. For a simple class like this, the effect is intuitive — b ends up with the same x and y as a — but the mechanism behind it is worth understanding precisely, especially once a class owns a resource.
class Point
{
public:
double x;
double y;
};
Point a{ 1.0, 2.0 };
Point b = a; // a copy operation runs hereThe Copy Constructor
A copy constructor is a constructor invoked when a new object is initialized directly from an existing object of the same type.
Point(const Point& other) is the copy constructor. Constructing b from a invokes it, copying a's x and y into b.
class Point
{
public:
Point(double xValue, double yValue) : x(xValue), y(yValue) {}
Point(const Point& other) : x(other.x), y(other.y) {}
double x;
double y;
};
Point a(3.0, 4.0);
Point b(a); // copy constructorCopy Constructor Syntax
A copy constructor takes a single parameter that is a reference (typically a const reference) to the same class type: ClassName(const ClassName& other). It must take its parameter by reference rather than by value — taking it by value would itself require making a copy of the argument to pass it, which would need the copy constructor again, an infinite recursion.
The Copy Assignment Operator
A copy assignment operator is invoked when an already-existing object is assigned the value of another existing object of the same type.
b already exists at the point of b = a;, so this calls operator=, which overwrites b's members with a's.
class Point
{
public:
Point(double xValue, double yValue) : x(xValue), y(yValue) {}
Point& operator=(const Point& other)
{
x = other.x;
y = other.y;
return *this;
}
double x;
double y;
};
Point a(3.0, 4.0);
Point b(0.0, 0.0);
b = a; // copy assignmentCopy Assignment Operator Syntax
A copy assignment operator has the signature ClassName& operator=(const ClassName& other). It returns a reference to *this so assignments can be chained, as in a = b = c;, and it typically takes its parameter by const reference for the same reasons as the copy constructor.
Copy Constructor vs Copy Assignment: When Each Is Invoked
The deciding factor is whether a new object is being brought into existence (constructor) or an already-existing object is being overwritten (assignment) — not the presence of the = symbol.
Situation | Which is called
Point b(a); | Copy constructor
Point b = a; | Copy constructor (copy-initialization of a new object)
b = a; (b already exists) | Copy assignment operator
Passing an object by value to a func. | Copy constructor
Returning a local object by value | Copy or move constructor, often elided (see Move Semantics)Copy Initialization
Point b = a; is called copy initialization. Despite the = symbol, this does not call operator= — b does not exist yet at that point, so there is nothing to assign to; the compiler instead constructs b directly using the copy constructor.
b's copy constructor runs, using a as its source.
Point a(1.0, 2.0);
Point b = a; // copy initialization: invokes the copy constructor, not operator=Direct Initialization vs Copy Initialization
For a simple case like this, both forms end up calling the same copy constructor. The practical difference between direct- and copy-initialization matters more when implicit conversions or explicit constructors are involved, as covered in the Constructors & Destructors topic.
Point c(a); // direct initialization
Point d = a; // copy initializationMember-wise (Default) Copying
If a class does not declare its own copy constructor or copy assignment operator, the compiler implicitly generates them, subject to conditions covered later in this topic. The implicit versions perform member-wise copying: each data member is copied using its own type's copy behavior.
Because Point declares no copy operations of its own, the compiler-generated copy constructor copies x and y individually.
class Point
{
public:
double x;
double y;
};
Point a{ 1.0, 2.0 };
Point b = a; // implicit member-wise copy: b.x = a.x, b.y = a.yShallow vs Deep Copy
A shallow copy duplicates a data member's own value directly, without duplicating what that value refers to. For a pointer data member, a shallow copy copies the pointer itself — the address — so the copy ends up pointing to the same object as the original, rather than to a separate copy of it. A deep copy instead duplicates the resource a member refers to, producing an independent copy the new object owns separately.
After this copy, a.data and b.data point to the very same array — there is only one underlying int[3], shared by two Buffer objects, even though they appear to be two independent objects.
This copy constructor allocates a new array of its own and copies each element's value across. b.data now points to a separate array from a.data — modifying one Buffer's contents no longer affects the other.
class Buffer
{
public:
explicit Buffer(int sizeValue) : size(sizeValue), data(new int[sizeValue]) {}
// no user-defined copy constructor: the implicit one performs a shallow copy
int size;
int* data;
};
Buffer a(3);
Buffer b = a; // shallow copy: b.data holds the same address as a.dataclass Buffer
{
public:
explicit Buffer(int sizeValue)
: size(sizeValue), data(new int[sizeValue])
{
}
Buffer(const Buffer& other)
: size(other.size), data(new int[other.size])
{
for (int i = 0; i < size; ++i)
{
data[i] = other.data[i];
}
}
~Buffer()
{
delete[] data;
}
int size;
int* data;
};Shallow vs Deep Copy Comparison
Whether shallow copying is a problem depends entirely on whether the copied member is treated as owning a resource.
Aspect | Shallow Copy | Deep Copy
What is copied | The member's own value | The value, plus a new copy of what it refers to
Resulting objects | Share the same underlying resource | Each owns an independent resource
Default for a raw pointer member | Yes (compiler-generated) | No — must be written by hand
Risk with owning resources | Double-free, unintended aliasing | None from sharing, at the cost of copy time/memory
Appropriate for | Non-owning members (observers) | Members that own a resource exclusivelyPointer/Resource Ownership Implications
When a class's data member is a raw pointer that the class treats as owning a resource — meaning the class's destructor deletes it — a shallow copy creates two objects that both believe they own the same resource. If both objects' destructors delete it, the resource is deleted twice, which is undefined behavior; and if one object modifies the shared resource, the change becomes visible through the other object as well, even though the two are supposed to be independent.
This scenario is described for understanding, not as code to run: two Buffer objects sharing a shallow-copied data pointer, each with a destructor that runs delete[] data, results in the same memory being deleted twice when both objects go out of scope — undefined behavior, not a specific guaranteed crash or message.
Implementing a Deep-Copying Class
A class that owns a resource typically needs its copy constructor, copy assignment operator, and destructor written together, so all three treat the resource consistently. This trio is exactly what the Rule of Zero/Three/Five topic addresses in depth.
The copy assignment operator allocates and fills the replacement array (newData) before releasing the old one (delete[] data;). Allocating first means that if the allocation were to throw, the object being assigned to is left unchanged rather than left holding a released pointer.
class Buffer
{
public:
explicit Buffer(int sizeValue)
: size(sizeValue), data(new int[sizeValue])
{
}
Buffer(const Buffer& other)
: size(other.size), data(new int[other.size])
{
for (int i = 0; i < size; ++i)
{
data[i] = other.data[i];
}
}
Buffer& operator=(const Buffer& other)
{
if (this == &other)
{
return *this;
}
int* newData = new int[other.size];
for (int i = 0; i < other.size; ++i)
{
newData[i] = other.data[i];
}
delete[] data;
data = newData;
size = other.size;
return *this;
}
~Buffer()
{
delete[] data;
}
int size;
int* data;
};Self-Assignment
Self-assignment occurs when an object is assigned to itself, as in a = a;, possibly indirectly through a reference or pointer alias, as in a = *p; where p happens to point to a. A copy assignment operator that releases its own resource before finishing reading from the source can corrupt the object when the source and target are the same object.
If the copy assignment operator above released data with delete[] data; before reading from other, and other happened to be the same object as *this, the loop copying other.data[i] would be reading from memory that had already been released — undefined behavior. This is why the guard shown next matters.
Guarding Against Self-Assignment
The if (this == &other) return *this; check at the top of the copy assignment operator shown earlier detects self-assignment and returns immediately, before any resource is released, avoiding the corruption described above. The copy-and-swap idiom, mentioned later in this topic, sidesteps the need for this explicit check entirely.
Copying const Objects
A const object can be used as the source of a copy, because copying only requires reading the source, and a const object supports being read. A const object cannot be the target of copy assignment, because assignment writes to *this, and a const object cannot be modified — attempting to assign to a const object fails to compile.
Taking the copy constructor's parameter by const reference is exactly what allows it to bind directly to a const source object like a.
const Point a(1.0, 2.0);
Point b(a); // OK: copy constructor only reads from a
Point c(3.0, 4.0);
// a = c; // Error: a is const, cannot be assigned toWhen Copy Operations Are Implicitly Generated
If a class does not declare a copy constructor, the compiler implicitly declares one, and similarly for copy assignment, provided nothing makes an implicit version impossible. The implicit versions perform member-wise copy or assignment, calling each member's own copy constructor or copy assignment operator in turn. Declaring a move constructor or move assignment operator suppresses the implicit copy operations; declaring a destructor alone does not, though relying on that particular combination is deprecated. The full rules for how these special member functions interact are the subject of the Rule of Zero/Three/Five topic.
When Copy Operations Are Implicitly Deleted
The implicitly-generated copy constructor or copy assignment operator is defined as deleted, rather than working member-wise, in certain situations — for example, when a member's own type has no accessible copy operation. The copy assignment operator specifically is also implicitly deleted if the class has a non-static data member of reference type, or a non-static data member of const (non-class) type, because assignment must modify an already-existing object, and a reference cannot be re-bound, nor can such a const member be reassigned. The copy constructor does not share this restriction, since initialization — unlike assignment — can set a const or reference member's value once, at construction.
HasReference can still be copy-constructed, but not copy-assigned, because of its reference data member.
class HasReference
{
public:
explicit HasReference(int& r) : ref(r) {}
int& ref;
};
// HasReference b(a); // OK: copy constructor can initialize ref from a.ref
// b = a; // Error: copy assignment is implicitly deleted; ref cannot be reboundThe Copy-and-Swap Idiom
Copy-and-swap is a named pattern for implementing copy assignment: the parameter is taken by value (which already invokes the copy constructor to produce a local copy), and that local copy is then exchanged with *this using a non-throwing swap. It automatically handles self-assignment correctly without an explicit check, and gives a strong exception-safety guarantee. Its full implementation details and exception-safety reasoning go beyond this topic's scope — it is mentioned here as a named idiom worth recognizing.
other is already an independent copy, paid for when the parameter itself was constructed. Swapping leaves the old data in other, which is then cleaned up automatically when other's destructor runs at the end of the function.
Buffer& operator=(Buffer other) // note: parameter taken by value
{
std::swap(size, other.size);
std::swap(data, other.data);
return *this;
}Best Practices
1. If a class doesn't manage a resource, let the compiler-generated copy constructor and copy assignment operator handle copying — don't hand-write them for classes that only hold ordinary values or other well-behaved class-type members.
2. If a class manages a resource through a raw owning pointer, give it a copy constructor and copy assignment operator that perform a deep copy, matching the destructor that releases that resource.
3. Guard a hand-written copy assignment operator against self-assignment, either with an explicit this == &other check or by using the copy-and-swap idiom.
4. Take copy constructor and copy assignment parameters by const reference, both to avoid an extra copy of the argument and to allow copying from const source objects.
5. Have copy assignment operators return *this by reference so assignments can be chained.
6. Prefer standard-library owning types, such as std::string or std::vector, as data members over raw owning pointers where possible — they already implement correct deep-copy behavior, removing the need to hand-write it.
Complete Example
b is deep-copied from a, so modifying b.data[0] does not affect a.data[0]. c is deep-copy-assigned from a, replacing its own smaller array with a copy of a's array.
a is created with size 3, data[0] set to 10 b is copy-constructed from a: an independent array, data[0] initially also 10 b.data[0] is changed to 99, but a.data[0] remains 10 c starts with size 2, then copy assignment from a gives it a new array matching a's contents The program's output.
#include <iostream>
class Buffer
{
public:
explicit Buffer(int sizeValue)
: size(sizeValue), data(new int[sizeValue])
{
for (int i = 0; i < size; ++i)
{
data[i] = 0;
}
}
Buffer(const Buffer& other)
: size(other.size), data(new int[other.size])
{
for (int i = 0; i < size; ++i)
{
data[i] = other.data[i];
}
}
Buffer& operator=(const Buffer& other)
{
if (this == &other)
{
return *this;
}
int* newData = new int[other.size];
for (int i = 0; i < other.size; ++i)
{
newData[i] = other.data[i];
}
delete[] data;
data = newData;
size = other.size;
return *this;
}
~Buffer()
{
delete[] data;
}
int size;
int* data;
};
int main()
{
Buffer a(3);
a.data[0] = 10;
Buffer b(a); // deep copy via copy constructor
b.data[0] = 99;
std::cout << a.data[0] << ' ' << b.data[0] << '\n';
Buffer c(2);
c = a; // deep copy via copy assignment
std::cout << c.size << ' ' << c.data[0];
return
…10 99
3 10Mental Model
Picture the difference between shallow and deep copying as whether the copy points at the same box, or gets a box of its own.
Shallow copying duplicates the pointer, leaving one shared resource; deep copying duplicates the resource itself, leaving two independent ones.
Shallow copy:
a.data ---.
+--> [ 10 | 20 | 30 ] (one array, two owners)
b.data ---'
Deep copy:
a.data ---> [ 10 | 20 | 30 ] (a's own array)
b.data ---> [ 10 | 20 | 30 ] (b's separate array, same contents)Quick Reference
A quick-reference summary of the copy-semantics mechanics covered in this topic.
Concept | Example | Meaning
Copy constructor | ClassName(const ClassName&) | Builds a new object from an existing one
Copy assignment operator | ClassName& operator=(const ClassName&) | Overwrites an existing object
Copy initialization | Point b = a; | New object, invokes copy constructor
Direct initialization | Point b(a); | New object, invokes copy constructor
Assignment | b = a; (b already exists) | Invokes copy assignment operator
Shallow copy | Copies a pointer's address | Shared resource
Deep copy | Allocates and copies contents | Independent resource
Self-assignment guard | if (this == &other) return *this; | Prevents corrupting self-assignmentKey Takeaways
The copy constructor builds a new object from an existing one; the copy assignment operator overwrites an already-existing object — the deciding factor is whether a new object is being created.
Without user-declared copy operations, the compiler generates member-wise copying, which is a shallow copy for pointer members.
A shallow copy of an owning pointer leaves two objects sharing one resource, risking a double-free and unintended aliasing; a deep copy gives each object its own independent resource.
A hand-written copy assignment operator must guard against self-assignment, or use the copy-and-swap idiom, which handles it automatically.
Copy constructor and copy assignment parameters should be taken by const reference, both for efficiency and to allow copying from const objects.
The copy assignment operator is implicitly deleted when a class has a reference member or a const (non-class) member, because assignment cannot rebind or reassign them.
Given Point b(a); and b = a; (where b already exists in the second case), which special member function does each invoke, and what determines the answer?
Point b(a); invokes the copy constructor, because b is being brought into existence for the first time from a. b = a; invokes the copy assignment operator, because b already exists and is being overwritten. The deciding factor is always whether a new object is being constructed or an existing object is being assigned to — not the presence of the = symbol, which appears in copy-initialization too.
What is the difference between a shallow copy and a deep copy of an object with a raw owning pointer member, and why does the difference matter for that object's destructor?
A shallow copy copies the pointer's own value (the address), so the copy and the original end up pointing at the same underlying resource. A deep copy allocates a new resource and copies the contents into it, so the copy owns an independent resource. It matters for the destructor because if both a shallow-copied original and its copy have destructors that delete the same shared resource, that resource is deleted twice — undefined behavior.
Why must a copy constructor's parameter be a reference rather than a plain value parameter?
Passing a class-type argument by value requires making a copy of it to place into the parameter, which for this particular function would mean invoking the copy constructor itself to construct its own parameter — an infinite recursion. Taking the parameter by reference (typically const reference) avoids that recursion entirely, since binding a reference does not require a copy.
Why does a self-assignment guard like if (this == &other) return *this; matter in a hand-written copy assignment operator for a class that owns a resource, but not in the compiler-generated member-wise version?
A hand-written copy assignment operator for a resource-owning class typically releases its current resource before copying the source's resource in. If the source and target happen to be the same object, releasing the resource first destroys the very data the rest of the function is about to read from, corrupting the object. The guard detects this case and returns immediately before any release happens. The compiler-generated member-wise version doesn't have this problem because it copies each member's value directly without any explicit release step of its own.
Why is the copy assignment operator implicitly deleted for a class with a non-static reference data member, while the copy constructor for the same class is not?
Assignment modifies an already-existing object, and a reference member cannot be re-bound to refer to a different object once it has been initialized — there is no way for copy assignment to make the reference member refer to the source's referent. Initialization, which the copy constructor performs, sets the reference member's binding exactly once, at construction, which is exactly what a reference supports — so the copy constructor works fine even though copy assignment cannot.
Trace through this code. What does it print, and why does modifying *b.value affect what a.value points to?
Box has no user-defined copy constructor, so the compiler-generated one performs a member-wise (shallow) copy of the pointer member value: b.value ends up holding the same address as a.value, rather than a new allocation. Because of this, *b.value = 99; writes through a pointer that a.value also points to, so a's pointed-to int changes as well. Output: 99.
Write a class named IntBox that owns a single dynamically allocated int (via new int(v) in its constructor). Give it a correct deep-copying copy constructor and copy assignment operator (with a self-assignment guard) and a destructor that deletes the int. Demonstrate that copying an IntBox and modifying the copy's value does not affect the original's value.
The copy constructor allocates a fresh int, initialized from *other.value, rather than copying the pointer itself, so a and b own independent ints. Changing *b.value to 99 leaves *a.value at 5. The program prints 5 99.
Given this class with a const int data member, why is the line Tag c(a); valid while the commented-out line b = a; would fail to compile if uncommented?
Tag's implicit copy constructor can initialize c's const id member directly from a.id, because initialization is allowed to set a const member's value once — this is valid, and c.id becomes 1, so the program prints 1. Tag's implicit copy assignment operator, however, is deleted, because assigning to b would require overwriting b.id, and a const member cannot be reassigned after it has already been initialized; that line would fail to compile.
Write a class named Name that owns a dynamically allocated char array (via new char[length + 1], copying the characters and a terminating null, from a const char* constructor argument using a simple manual loop, not std::string). Implement a correct deep-copying copy constructor, copy assignment operator (with self-assignment guard), and destructor. Demonstrate copying a Name and printing both the original and the copy.
The copy constructor allocates its own array sized for other.length + 1 characters (including the null terminator) and copies each character across, so a and b own separate arrays with equal contents. Both a.data and b.data print as the C-string "Ada", giving output Ada Ada, and destroying either object only releases that object's own array.
class Box
{
public:
Box(int v) : value(new int(v)) {}
// no user-defined copy constructor
int* value;
};
Box a(5);
Box b = a;
*b.value = 99;
std::cout << *a.value;class Tag
{
public:
const int id;
};
Tag a{ 1 };
Tag b{ 2 };
Tag c(a);
// b = a;
std::cout << c.id;