Lesson 05 / 14
Copying and Assignment
Measures that the copy constructor and copy assignment operator produce a deep copy that takes its own array, and that the depth of the copy depends on the design of the class.
Published Updated
In this lesson
- Distinguishes when the copy constructor is called from when copy assignment is called
- Measures that a class taking its own array produces a deep copy
- Applies why the self-assignment check is necessary
Contents
The previous lesson claimed that a guard holds its resource by value, so when the guard is
copied, its resource is copied along with it. This lesson measures that claim: when an
object is copied, does the copy share the same memory as the original, or does it take its
own memory? The measurement kind is copy: b, constructed from a with
Buffer b(a), is a newly constructed object, different from a — but how deep that
“different” runs depends on how the class’s copy constructor is written.
In C, copying an array was always an explicit operation: memcpy is called by hand,
the destination being large enough is checked by hand, and the source and destination not
overlapping is guaranteed by hand. In C++, when a class object is copied via = or passed
by value as an argument, the copying is implicit — the compiler decides on its own which
function to call, no explicit call is written by hand. This makes copying easier, but it
also hides it: seeing a line like Buffer b(a); and understanding how much work it does
(allocating new memory, copying element by element) requires looking at the class’s
definition — the line itself does not say.
Copy Constructor and Copy Assignment
A class’s copy constructor is a constructor that takes a const& of the same class —
Buffer(const Buffer& other). This constructor is called during a construction, as in
Buffer b(a); or Buffer b = a;: b does not yet exist, and is constructed from a.
Copy assignment, on the other hand, is an overload of operator= —
Buffer& operator=(const Buffer& other). This is called during an assignment, as in
b = a;: b already exists (it was constructed earlier), and only its value is replaced
with a’s. Both take const Buffer& as their argument type, but one constructs a new
object while the other changes the contents of an existing object — this difference is what
determines which one is called: whether the object on the left is new or already existing.
This distinction is not merely syntactic. While the copy constructor is running, the
object’s data members are not yet constructed — the same rule from the third lesson’s
member initializer list applies here too: data_(new int[other.size_]) initializes the
member for the first time. While copy assignment is running, on the other hand, the object
is already constructed, and data_ already points at a valid array — this is why
operator= must first release the old array (delete[] data_); otherwise the old array
is never freed while a new one is assigned over it, which is a memory leak. The copy
constructor has no such prior state, because the object does not yet exist — this is why the
copy constructor never calls delete[], it only allocates.
// buffer.hpp — a class holding its own array, copying deeply
#ifndef BUFFER_HPP
#define BUFFER_HPP
#include <cstdio>
class Buffer {
public:
explicit Buffer(int size) : size_(size), data_(new int[size]) {
for (int i = 0; i < size_; ++i) {
data_[i] = i;
}
std::printf("constructor ran size=%d\n", size_);
}
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
for (int i = 0; i < size_; ++i) {
data_[i] = other.data_[i];
}
std::printf("copy constructor ran size=%d\n", size_);
}
Buffer& operator=(const Buffer& other) {
if (this == &other) {
return *this;
}
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
for (int i = 0; i < size_; ++i) {
data_[i] = other.data_[i];
}
std::printf("copy assignment ran size=%d\n", size_);
return *this;
}
~Buffer() {
delete[] data_;
}
int first_element() const { return data_[0]; }
void set_first_element(int value) { data_[0] = value; }
const int* address() const { return data_; }
private:
int size_;
int* data_;
};
#endif
A Deep Copy Takes Its Own Array
// copy-measurement.cpp — is b, freshly constructed from a, a copy that takes its own array
#include <cstdio>
#include "buffer.hpp"
int main() {
Buffer a(3);
Buffer b(a);
std::printf("do a and b share an array %s\n", a.address() == b.address() ? "yes" : "no");
b.set_first_element(99);
std::printf("a first element %d\n", a.first_element());
std::printf("b first element %d\n", b.first_element());
return 0;
}
constructor ran size=3 copy constructor ran size=3 do a and b share an array no a first element 0 b first element 99
CR10. Buffer b(a); constructs b from a with the copy constructor; the copy
constructor reads other.size_ and allocates a separate array with its own call to
new int[other.size_], then copies other.data_’s elements into this new array one by
one. This is a deep copy: b.data_ and a.data_ are different addresses,
a.address() == b.address() prints no. This is why b.set_first_element(99) changes
only b’s own array; a.first_element() is still 0. b is of kind copy relative to
a: newly constructed, different from a in address, but equal to it in the value it held
at the moment of construction.
Copy Assignment and the Self-Assignment Check
// assignment-measurement.cpp — does b = a run copy assignment
#include <cstdio>
#include "buffer.hpp"
int main() {
Buffer a(2);
Buffer b(5);
b.set_first_element(-1);
a.set_first_element(42);
b = a;
std::printf("after assignment is b's first element the same as a's %s\n",
b.first_element() == a.first_element() ? "yes" : "no");
std::printf("do a and b share an array %s\n", a.address() == b.address() ? "yes" : "no");
b = b;
std::printf("after self-assignment is b still valid %s\n", b.first_element() == a.first_element() ? "yes" : "no");
return 0;
}
constructor ran size=2 constructor ran size=5 copy assignment ran size=2 after assignment is b's first element the same as a's yes do a and b share an array no after self-assignment is b still valid yes
CR11. In the line b = a;, b is already a constructed object, so copy assignment is
called, not the copy constructor. operator= first releases b’s old array with
delete[] data_, then allocates a new array of the same length as a’s and copies
a.data_’s contents into it — this is why, after the assignment, b’s first element
equals a’s (the contents were copied), but b.address() is not equal to
a.address() (a new array was allocated, not shared). b’s address before the assignment
is not separately printed and compared here: whether the new that immediately follows the
delete[] returns the same address depends on the allocator, and the C++ standard does not
guarantee this — so such a comparison would not count as evidence, only the contents and
the not-shared-with-a state are measured. The line b = b; triggers the
if (this == &other) return *this; check at the start of operator=: without this check,
the function would first release b’s own array with delete[], then try to read
other.data_ (which is now the same, already-released, invalid address) — this would be
undefined behavior. Thanks to the check, b = b; safely returns early, and b remains
unchanged.
The Risk of a Shallow Copy, and the Rule of Three
If Buffer’s copy constructor and copy assignment operator were not written by hand, the
compiler would use its own generated default versions — these copy every data member
member by member. Because size_ is an int, this causes no problem, but data_ is an
int*; member-by-member copying copies the pointer itself, not the array it points at.
This is a shallow copy: two Buffers would share the same data_ address. The result of
a shallow copy is this — when one is destroyed, it runs delete[] data_, while the other
still points at the same (now released) array; when that object is also destroyed, it tries
to release the same array again with delete[]. This double free is undefined
behavior, and this lesson does not run it — it is only mentioned in prose; Buffer is
always run with the hand-written deep copy constructor and assignment.
This observation leads to the rule of three: if a class has to write its destructor,
copy constructor, or copy assignment operator by hand (usually because it holds a resource
through a raw pointer), it must write all three by hand — if one is written by hand and the
other two are left to the compiler, the risk of a shallow copy arises. Buffer fully
applies this rule: the destructor, the copy constructor, and copy assignment, all three
manage data_ correctly.
There is a reason the rule is limited to “three”: all three are different faces of the same
problem — ownership of the resource. The destructor defines when the resource is released,
the copy constructor defines how the resource is duplicated, and copy assignment defines
how an existing resource is changed. If one is forgotten (for example, only the destructor
is written and the copy constructor is left to the compiler), the compiler-generated
shallow copy constructor shares data_ — then two objects each try to release the same
array with delete[], which is a double free. The mere existence of a destructor is itself
a sign that the other two must also be written by hand: if a class does something special in
its destructor (something beyond just delete[]), this usually means the class is the
owner of a resource, and ownership must also be thought through during copying.
Preventing Copying with = delete
Not every class needs to be copyable. If a resource is meant to have a single owner — for
example, a file handle or a network connection should not be shared with a second object —
the copy constructor and copy assignment can be explicitly deleted with = delete:
Buffer(const Buffer&) = delete;. This does not tell the compiler to never generate that
function, but to give a compile error when it is used — code that tries to write
Buffer b(a); does not compile. Buffer remains copyable for this lesson’s measurement,
= delete is only mentioned here; which classes should be designed as non-copyable, and how
that decision relates to the ownership model, is K05’s topic.
Why the Copy Constructor’s Argument Is const&
The copy constructor’s argument is taken as const Buffer&, not as Buffer. If the
argument were taken by value (Buffer other), calling the function would itself require a
copy of other — and the function that would construct that copy would be the copy
constructor itself, an infinite loop. Taking it by reference breaks this loop: the original
object is accessed directly, with no extra copy produced. const, meanwhile, guarantees
that other will only be read, not modified during the copy — the copy constructor must
not modify its source, only read from it.
A Copy Does Not Reveal the Depth of the Source
Like the Guard from the previous lesson, Buffer also holds its resource by value — but
while Guard’s resource_ member is a Resource value, Buffer‘s data_ member is
an int*; the resource (the array) lives on the heap, and the member is only a pointer to
it. This lesson closes that gap by writing Buffer by hand to copy deeply, but the
depth of a copy is never automatic — it depends on what the class’s copy constructor
writes. If a class is tied to a record shared through a raw pointer (for example, if the
int* only points at an array owned by another object, and ownership is not with
Buffer), member-by-member copying also copies this pointer, and the copy shares the same
record as the original — a deep copy only happens when the class is the owner of its
own resource, and the copy constructor explicitly reallocates it.
This means that different members of the same class can call for different copying
behavior. If Buffer also had a member named const int* reference_, pointing at a
resource it does not own, it would be correct for the copy constructor to deliberately
copy this member shallowly — copying the pointer as it is, without allocating new memory —
because Buffer is not the owner of that resource, it only looks at it. The question that
must be asked when writing a copy constructor is this: is this member a resource this
object owns, or only a resource it looks at? The answer can differ from member to member,
and the copy constructor’s body is the sum of these decisions — the compiler-generated
default member-by-member copy never makes this distinction, it copies every member the same
way (shallowly).
Move — a resource being transferred by taking it over rather than by copying it — solves this problem in a different way, and is outside this course’s scope (K05). M11/K01 covered a struct being copied whole, field by field, through assignment, and memory layout — member order, alignment, and padding; this lesson combines the two: copying conforms both to value semantics (a copy is independent of the original) and to memory management (a copy takes its own memory) at the same time.
The Bounding Measurement
The expression this lesson measures is of kind copy: b is newly constructed from a,
an object separate from a in address. But a copy does not, by itself, say how “deep” it
is — this information is not in the result of the copy, it is hidden in how the class’s
copy constructor behaves with its source. Buffer copies deeply because its copy
constructor allocates a new array with new int[...]; a class tied to a resource through a
raw pointer, one that has not written its own copy constructor correctly, copies shallowly,
and the copy shares the same resource as the original. To read a class and answer the
question “can this be copied, and what happens when it is copied,” you have to look at the
body of the copy constructor — the class’s name or the way it is used does not say.
Summary
- The expression this lesson measures is of kind copy:
Buffer b(a)is newly constructed froma, an object separate from it in address. - The copy constructor is called during a construction, copy assignment during an assignment; whether the object on the left is new or already existing determines which one runs.
Bufferproduces a deep copy by allocating its own array withnew; member-by-member copying would only copy the pointer, which would be a shallow copy.- Copy assignment safely handles self-assignment with the
this == &othercheck. - If one of the destructor, the copy constructor, or copy assignment is written by hand, all three must be written (the rule of three).
Next Step
This lesson measured that an object takes its own resource when it is copied; the next lesson moves away from copying and measures how a reference directly modifies the caller’s object — producing no copy at all.
To keep your progress and take notes, Log in
My notes
Log in to take notes.