Lesson 7 of 12
Copying happens constantly, often without a visible =:
#include <iostream>
struct Point {
int x;
int y;
};
void take_by_value(Point p) { // a copy is made here
p.x = 999;
std::cout << "inside the function, p.x = " << p.x << "\n";
}
Point make() {
Point local{7, 8};
return local; // and here
}
int main() {
Point a{1, 2};
Point b = a; // copy construction
Point c{0, 0};
c = a; // copy assignment
b.x = 100;
std::cout << a.x << " " << b.x << " " << c.x << "\n";
take_by_value(a);
std::cout << "after by-value call, a.x = " << a.x << "\n";
Point d = make();
std::cout << d.x << "," << d.y << "\n";
return 0;
}
1 100 1
inside the function, p.x = 999
after by-value call, a.x = 1
7,8
Two distinct operations, and the difference matters:
- Copy construction — building a new object from an existing one.
Point b = a; - Copy assignment — overwriting an already existing object.
c = a;
The first has no old state to clean up. The second does, which is what makes it harder.
What the compiler generates
If you write neither, the compiler produces both, and they copy each member in turn. For
Point that is exactly right: copying two ints produces an independent Point.
For members that manage resources it is also right, because the member's own copy operations run.
std::string and std::vector copy themselves properly, so a class made of them copies properly
for free:
#include <iostream>
#include <string>
#include <vector>
struct Record { // no copy operations written
std::string name;
std::vector<int> scores;
};
int main() {
Record a{"Sokha", {90, 85, 77}};
Record b = a; // compiler-generated copy
b.name = "Dara";
b.scores[0] = 100;
std::cout << a.name << " " << a.scores[0] << "\n";
std::cout << b.name << " " << b.scores[0] << "\n";
return 0;
}
Sokha 90
Dara 100
Fully independent, and not a line of copy code written. This is the normal case — most classes need nothing.
Where it goes wrong
The exception is a class holding a raw pointer it owns. Recall Buffer from lesson 9.4:
class Buffer {
int* data;
std::size_t count;
// ...
};
Member-by-member copying copies the pointer, not what it points at. Two Buffers then hold the
same address:
b1 b2 (a copy of b1)
+----------+ +----------+
| data ----+---+ +--+---- data |
| count 4 | | | | count 4 |
+----------+ v v +----------+
+------------+
| 0 0 0 0 | one array, two owners
+------------+
Both destructors will run. Both will delete[] the same address. That is a double free —
undefined behaviour, and in practice a crash or heap corruption.
This is a shallow copy: the handle is duplicated, the resource is not.
Writing the copy operations
#include <iostream>
class Buffer {
int* data;
std::size_t count;
public:
Buffer(std::size_t n) : data(new int[n]), count(n) {
for (std::size_t i = 0; i < count; ++i) {
data[i] = 0;
}
}
~Buffer() { delete[] data; }
// Copy constructor: allocate our own array, then copy the contents.
Buffer(const Buffer& other) : data(new int[other.count]), count(other.count) {
for (std::size_t i = 0; i < count; ++i) {
data[i] = other.data[i];
}
std::cout << "deep-copied " << count << " ints\n";
}
// Copy assignment: we already own something, so release it first.
Buffer& operator=(const Buffer& other) {
if (this == &other) {
return *this; // self-assignment: nothing to do
}
delete[] data; // release what we had
count = other.count;
data = new int[count];
for (std::size_t i = 0; i < count; ++i) {
data[i] = other.data[i];
}
std::cout << "deep-assigned " << count << " ints\n";
return *this;
}
void set(std::size_t i, int v) { data[i] = v; }
int get(std::size_t i) const { return data[i]; }
};
int main() {
Buffer a(3);
a.set(0, 42);
Buffer b = a; // copy constructor
b.set(0, 99);
Buffer c(1);
c = a; // copy assignment
std::cout << a.get(0) << " " << b.get(0) << " " << c.get(0) << "\n";
return 0;
}
deep-copied 3 ints
deep-assigned 3 ints
42 99 42
b changed without disturbing a. Three independent arrays, three clean destructor calls.
The four things copy assignment must get right
Compare the two signatures:
Buffer(const Buffer& other); // copy constructor
Buffer& operator=(const Buffer& other); // copy assignment
Copy assignment must additionally:
1. Handle self-assignment. Without the this == &other check, a = a; would delete[] data
and then copy from the array it just freed — a use-after-free reading its own corpse.
2. Release the old resource. The object already owns something. Forget the delete[] and
every assignment leaks.
3. Return Buffer&. So a = b = c; chains, matching how assignment works for built-in types.
4. Take a const&. Taking by value would need a copy to make the copy — infinite recursion.
Why const Buffer& and not Buffer
The copy constructor's parameter must be a reference. If it took Buffer other by value, making
that parameter would require calling the copy constructor, which would require making its
parameter, and so on. The compiler rejects it outright:
error: invalid constructor; you probably meant 'Buffer (const Buffer&)'
The better answer
Everything above is correct and about forty lines you should usually not write. Buffer exists to
teach the mechanism. In real code:
#include <iostream>
#include <vector>
class Buffer {
std::vector<int> data; // the vector handles everything
public:
Buffer(std::size_t n) : data(n, 0) {}
void set(std::size_t i, int v) { data[i] = v; }
int get(std::size_t i) const { return data[i]; }
std::size_t size() const { return data.size(); }
};
int main() {
Buffer a(3);
a.set(0, 42);
Buffer b = a; // correct, and no copy code written
b.set(0, 99);
std::cout << a.get(0) << " " << b.get(0) << "\n";
return 0;
}
42 99
No destructor, no copy constructor, no copy assignment — and it is correct, exception-safe, and shorter. Lesson 9.11 names this the Rule of Zero.
Try it
Take the Buffer with the destructor but without the copy operations, and copy one. Predict
what happens, then run it under -fsanitize=address.
Checkpoint
- Copy construction builds a new object; copy assignment overwrites an existing one.
- The compiler generates both, copying member by member — correct for values and for self-managing members.
- A class owning a raw pointer gets a shallow copy: two owners, one resource, double free.
- A copy constructor takes
const T&— by value would recurse infinitely. - Copy assignment must check self-assignment, free the old resource, and return
T&. - Holding a
std::vectororstd::stringinstead makes all of this unnecessary.
Questions about this lesson
Log in to ask a question.