Skip to content

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:

  1. a copy constructor;
  2. a move constructor;
  3. a copy-assignment operator taking const T&;
  4. a move-assignment operator taking T&&;
  5. 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:

  1. how the marks are stored;
  2. exactly which of the five special members you will write, and which you will leave to the compiler — for each, one sentence saying why;
  3. the postconditions of the move constructor (what state the source is left in);
  4. how the destructor behaves for a moved-from object;
  5. 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:

  1. say what the array becomes;
  2. list the five operations and say, for each, whether it disappears or stays correct automatically;
  3. name the observable behaviour that must not change, and how you would test it;
  4. 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