Bank 8 — Copying and resource management¶
Twelve exercises on what the compiler's default copy and move operations actually do, and on the Rule of Three, Five and Zero. Module 10. Levels L1–L6.
Solutions are in Solutions and answers; nudges are in Hints. Attempt first.
Exercises¶
E82 — Shallow and deep L1 concept¶
Task. Define shallow copy and deep copy in one sentence each. Then explain
why the compiler's default copy constructor is a shallow copy, and why that is exactly
right for a class that has no raw resource but exactly wrong for one that has a newed
buffer.
E83 — Copied twice, freed twice L4 predict¶
#include <iostream>
class Buffer {
public:
explicit Buffer(int n) : n_(n), data_(new int[n]) {}
~Buffer() { delete[] data_; std::cout << "freed\n"; }
int* data() const { return data_; }
int n() const { return n_; }
private:
int n_;
int* data_;
};
int main() {
Buffer a{4};
Buffer b = a; // copy the pointer, not the buffer
std::cout << "done\n";
}
Task. Describe what the two objects share after the copy, and what goes wrong at the
end of main. Be precise about whether this is a leak, a double free, or undefined
behaviour, and say which of those the C++ standard actually promises.
E84 — Write the copy constructor L2 complete¶
#include <iostream>
class Buffer {
public:
explicit Buffer(int n) : n_(n), data_(new int[n]) {
for (int i = 0; i < n_; ++i) data_[i] = 0;
}
// TODO (1): a copy constructor that performs a DEEP copy
~Buffer() { delete[] data_; }
int* data() const { return data_; }
int n() const { return n_; }
private:
int n_;
int* data_;
};
int main() {
Buffer a{3};
a.data()[0] = 42;
Buffer b = a;
b.data()[0] = 7;
std::cout << a.data()[0] << ' ' << b.data()[0] << '\n';
}
Task. Write the copy constructor so the program prints 42 7 — that is, modifying
the copy does not touch the original. (The destructor already frees; you are only
allowed to add the copy constructor.)
E85 — Count the copies L2 trace scenario¶
A team instruments its record type to count how often it is copied, because copies were slowing things down.
#include <iostream>
#include <string>
#include <vector>
class Record {
public:
explicit Record(std::string id) : id_(std::move(id)) {
std::cout << "build " << id_ << '\n';
}
Record(const Record& other) : id_(other.id_) {
std::cout << "copy " << id_ << '\n';
}
~Record() { std::cout << "drop " << id_ << '\n'; }
private:
std::string id_;
};
int main() {
std::vector<Record> box;
box.reserve(2);
box.push_back(Record{"R1"});
box.push_back(Record{"R2"});
std::cout << "size " << box.size() << '\n';
}
Task. Write every output line in order. Note reserve(2) — there is no
reallocation. How many copies happened, and does Record have a move constructor to
avoid them? (Look at what the class declares.)
E86 — The Rule of Three L3 modify scenario¶
A register of readings owns a raw array. It has a destructor, but its copies double-free.
#include <iostream>
class Register {
public:
explicit Register(int n) : n_(n), data_(new double[n]) {
for (int i = 0; i < n_; ++i) data_[i] = 0.0;
}
~Register() { delete[] data_; }
void set(int i, double v) { if (i >= 0 && i < n_) data_[i] = v; }
double get(int i) const { return (i >= 0 && i < n_) ? data_[i] : 0.0; }
int size() const { return n_; }
private:
int n_;
double* data_;
};
int main() {
Register a{2};
a.set(0, 1.5);
Register b = a;
b.set(0, 9.0);
std::cout << a.get(0) << ' ' << b.get(0) << '\n';
}
Task. Apply the Rule of Three: add the copy constructor and copy-assignment operator
so the program prints 1.5 9. For the assignment, handle self-assignment explicitly
and say why the check is needed even though it "cannot happen".
E87 — Assigning to yourself L3 debug scenario¶
A tool copies a record onto itself by accident when a config value is repeated.
#include <iostream>
class Blob {
public:
explicit Blob(int n) : n_(n), data_(new int[n]) {}
~Blob() { delete[] data_; }
Blob& operator=(const Blob& other) {
delete[] data_; // (1) free our buffer
n_ = other.n_;
data_ = new int[n_]; // (2) allocate a new one
for (int i = 0; i < n_; ++i) data_[i] = other.data_[i];
return *this;
}
int n() const { return n_; }
private:
int n_;
int* data_;
};
int main() {
Blob a{3};
Blob& ref = a;
a = ref; // self-assignment
std::cout << a.n() << '\n';
}
Task. Explain why this crashes (or reads freed memory) on a = ref, and distinguish
it from the ordinary case. Then fix operator= so self-assignment is harmless, naming
the two ways to do it (a guard, and an idiom that needs no guard).
E88 — Move, don't copy L3 predict¶
#include <iostream>
#include <utility>
class Handle {
public:
explicit Handle(int id) : id_(id) { std::cout << "open " << id_ << '\n'; }
Handle(Handle&& other) noexcept : id_(other.id_) {
other.id_ = -1;
std::cout << "move " << id_ << '\n';
}
Handle& operator=(Handle&& other) noexcept {
id_ = other.id_;
other.id_ = -1;
std::cout << "move-assign " << id_ << '\n';
return *this;
}
~Handle() {
if (id_ != -1) std::cout << "close " << id_ << '\n';
}
int id() const { return id_; }
private:
int id_;
};
int main() {
Handle a{10};
Handle b = std::move(a);
std::cout << "a is " << a.id() << ", b is " << b.id() << '\n';
}
Task. Write all output lines in order. Why does the moved-from object print
close only once — i.e. why does the destructor stay quiet for a? What is the state
of a moved-from object, in general?
E89 — The Rule of Zero L4 analyze¶
Two classes hold the same logical data.
// Version A
class DocA {
std::string title_;
std::vector<std::string> lines_;
};
// Version B
class DocB {
explicit DocB(std::size_t n) : title_(new char[64]), lines_(new std::string[n]) {}
~DocB() { delete[] title_; delete[] lines_; }
// ... copy and move operators written by hand ...
char* title_;
std::string* lines_;
std::size_t count_;
};
Task. Which version needs destructor, copy and move operators written by hand, and
which needs none? State the Rule of Zero in one sentence, and explain why a class that
uses only std::string and std::vector members can never double-free.
E90 — The leak on reassignment L4 debug scenario¶
#include <iostream>
class Box {
public:
explicit Box(int n) : data_(new int[n]) {}
~Box() { delete[] data_; }
// no copy constructor, no copy assignment written
private:
int* data_;
};
int main() {
Box a{4};
Box b{4};
b = a; // what happens to b's original buffer?
std::cout << "done\n";
}
Task. Explain what happens to b's original buffer when b = a runs, given that the
compiler supplies a copy-assignment operator. Is the problem a leak, a double free, or
both? Then fix it in the way that avoids writing any of the five operations by hand.
E91 — Copy or steal? L4 analyze¶
For each operation, say whether it should copy the resource, steal (move) it, or neither, and why:
- a copy constructor;
- a move constructor;
- a copy-assignment operator taking
const T&; - a move-assignment operator taking
T&&; - a destructor.
Task. Then answer: why must move operations be marked noexcept to be used by
std::vector during reallocation, and what happens if they are not?
E92 — Design an owning container L5 design memory¶
A grade book owns a dynamically sized array of marks. It must be copyable (you may want two independent grade books) and efficiently movable (returning one from a function).
Task. Decide and write down:
- how the marks are stored;
- exactly which of the five special members you will write, and which you will leave to the compiler — for each, one sentence saying why;
- the postconditions of the move constructor (what state the source is left in);
- how the destructor behaves for a moved-from object;
- one thing you can do to make the Rule of Zero apply instead, and why you might prefer it.
How you will be judged. On whether the set of operations you write is minimal and complete, and on whether move leaves the source destructible.
E93 — Rewrite it without raw pointers L6 refactor memory scenario¶
A legacy Catalog owns a raw array of items and hand-writes all five operations. Two
bugs in the last year were both in the move-assignment operator.
Task. Plan a refactor to remove the raw pointer so the class needs no hand-written special members. Your plan must:
- say what the array becomes;
- list the five operations and say, for each, whether it disappears or stays correct automatically;
- name the observable behaviour that must not change, and how you would test it;
- state the one case where you might still need a hand-written operation, and why.
Success. After the refactor, "Rule of Zero" is true and you can say why.
Previous topic: Composition and association · Next topic: RAII and smart pointers · Hints: hints.md · Solutions: solutions.md