Copying
What it means to duplicate an object, and what the compiler writes for you.
By the end of this chapter you can
- Distinguish a shallow copy from a deep copy
- Write a correct copy constructor and copy assignment operator
- Explain the self-assignment problem
Copying happens more often than it looks. Passing by value copies. Returning by value may copy. Putting an object in a container copies. Assigning copies. For types made of other well-behaved types, all of that is correct and you never think about it — that is the rule of zero working.
This chapter is about the case where it is not automatic: a type that owns a raw resource, where the compiler’s guess is wrong.
What the compiler writes for you
If you do not declare them, the compiler generates a copy constructor and a copy assignment operator that copy each member in turn.
#include <iostream>
#include <string>
struct Pair {
std::string name;
int count;
};
int main() {
Pair a{"alpha", 1};
Pair b = a; // copy CONSTRUCTOR: b is being created
Pair c{"gamma", 3};
c = a; // copy ASSIGNMENT: c already exists
b.count = 99;
std::cout << "a: " << a.name << ' ' << a.count << '\n';
std::cout << "b: " << b.name << ' ' << b.count << '\n';
std::cout << "c: " << c.name << ' ' << c.count << '\n';
}The distinction matters because their jobs differ. A copy constructor builds a new object from nothing; a copy assignment operator has to deal with an object that already holds something, which it must release first.
For Pair both are correct as generated, because std::string knows how to
copy itself. Memberwise copying is right whenever every member’s copy is right.
Shallow versus deep
It stops being right when a member is a raw pointer, because copying a pointer duplicates the address, not the thing at the far end.
#include <cstddef>
#include <iostream>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(new int[n]{}), size_(n) {}
~Buffer() { delete[] data_; }
int& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
private:
int* data_;
std::size_t size_;
};
int main() {
Buffer a{4};
a[0] = 10;
Buffer b = a; // memberwise: b.data_ == a.data_
b[0] = 20;
std::cout << "a[0] = " << a[0] << " (we only wrote to b)\n";
} // both destructors delete the same pointerTwo problems, and only the second one crashes. Writing through b changed a,
because there is one array with two owners. Then both destructors ran
delete[] on it.
A deep copy allocates its own storage and duplicates the contents, so the two objects are genuinely independent:
#include <cstddef>
#include <iostream>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(new int[n]{}), size_(n) {}
~Buffer() { delete[] data_; }
// Deep copy: new storage, contents duplicated.
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];
}
int& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
private:
int* data_;
std::size_t size_;
};
int main() {
Buffer a{4};
a[0] = 10;
Buffer b = a;
b[0] = 20;
std::cout << "a[0] = " << a[0] << ", b[0] = " << b[0] << " — independent\n";
}Copy assignment, and the two traps
Assignment is harder than construction, because the target already owns something. The obvious version has two bugs:
#include <cstddef>
#include <iostream>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(new int[n]{}), size_(n) {}
~Buffer() { delete[] data_; }
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];
}
Buffer& operator=(const Buffer& other) {
delete[] data_; // release what we hold
data_ = new int[other.size_]; // ... but if other IS us,
size_ = other.size_; // we just freed the source
for (std::size_t i = 0; i < size_; ++i) data_[i] = other.data_[i];
return *this;
}
int& operator[](std::size_t i) { return data_[i]; }
private:
int* data_;
std::size_t size_;
};
int main() {
Buffer a{4};
a[0] = 7;
a = a; // self-assignment
std::cout << "a[0] = " << a[0] << '\n';
}Self-assignment. a = a looks absurd written out, but it arrives through
references and aliases — values[i] = values[j] where the indices happen to
match, or *p = *q where both point at the same object.
Follow what happens when other is *this. delete[] data_ frees the array
holding the 7. data_ = new int[...] installs a fresh, uninitialised array —
and because other is the same object, other.data_ now names that same fresh
array. The loop then copies the new array onto itself, element by element. The
original contents were freed two lines earlier and are simply gone.
Look at what the program printed: not 7. And look at what the sanitizers said: nothing. At the level of memory this program is impeccable — every allocation is freed exactly once, and every read is inside a live allocation. The bug is purely one of meaning, so no tool catches it. A silent, correct-looking object with the wrong contents is a considerably worse outcome than a crash.
Forgetting to return *this. Assignment returns a reference to the target
so that a = b = c chains. Omitting it is a compile error for a declared return
type, which is one bug the compiler does catch.
The direct fix is a self-check:
#include <cstddef>
#include <iostream>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(new int[n]{}), size_(n) {}
~Buffer() { delete[] data_; }
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];
}
Buffer& operator=(const Buffer& other) {
if (this == &other) return *this; // the guard
int* fresh = new int[other.size_]; // allocate BEFORE destroying
for (std::size_t i = 0; i < other.size_; ++i) fresh[i] = other.data_[i];
delete[] data_;
data_ = fresh;
size_ = other.size_;
return *this;
}
int& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
private:
int* data_;
std::size_t size_;
};
int main() {
Buffer a{4};
a[0] = 7;
a = a;
std::cout << "self-assignment survived: a[0] = " << a[0] << '\n';
Buffer b{2};
b = a;
std::cout << "b now has " << b.size() << " elements, b[0] = " << b[0] << '\n';
}Note the ordering: allocate the new buffer first, and only free the old one
once the allocation has succeeded. If new throws, the object is still exactly
as it was — the strong exception guarantee. The naive version, which frees
first, leaves a destroyed object behind if the allocation fails.
copy-and-swap
There is a well-known idiom that gets self-assignment safety and the strong guarantee without writing either explicitly:
#include <cstddef>
#include <iostream>
#include <utility>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(new int[n]{}), size_(n) {}
~Buffer() { delete[] data_; }
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];
}
// Take the parameter BY VALUE: the copy is made by the copy constructor,
// then swapped in. Self-assignment is harmless; a throw happens before
// anything is modified.
Buffer& operator=(Buffer other) {
swap(*this, other);
return *this;
} // `other` destructs here, taking the old buffer
friend void swap(Buffer& first, Buffer& second) noexcept {
std::swap(first.data_, second.data_);
std::swap(first.size_, second.size_);
}
int& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
private:
int* data_;
std::size_t size_;
};
int main() {
Buffer a{4};
a[0] = 7;
a = a;
Buffer b{2};
b = a;
std::cout << "a[0] = " << a[0] << ", b.size() = " << b.size() << '\n';
}One function instead of two branches, no self-check needed, and the old resources are released by the parameter’s destructor. The cost is that it always copies, even when assigning from something about to be destroyed — which is what the next chapter is about.
The cost of copying
Deep copies are correct, and they are not free:
#include <chrono>
#include <iostream>
#include <vector>
int main() {
std::vector<int> source(2'000'000, 1);
using clock = std::chrono::steady_clock;
using ms = std::chrono::milliseconds;
auto start = clock::now();
std::vector<int> copy = source; // deep copy: 8 MB duplicated
auto mid = clock::now();
std::vector<int> moved = std::move(source); // ownership transfer
auto finish = clock::now();
std::cout << "copy: " << std::chrono::duration_cast<ms>(mid - start).count() << " ms\n";
std::cout << "move: " << std::chrono::duration_cast<ms>(finish - mid).count() << " ms\n";
std::cout << "(copy has " << copy.size() << ", moved has " << moved.size()
<< ", source now has " << source.size() << ")\n";
}The copy duplicated eight megabytes. The move copied three pointers. When the source is about to be destroyed anyway, paying for the copy is pure waste — which is exactly the problem move semantics exists to solve.