Skip to content

Step 1 of 6 · Reading · ~4 min

Learn

Modern C++ Patterns

The Rule of Five and RAII: Copying an Owner

You will write a class that owns a heap array so it can be destroyed, copied and assigned safely, and know which members the compiler still writes when you don't.

Until now the owners (vector, unique_ptr) were written by someone else; here you write one. The copy constructor takes another object of the same class.

The compiler's copy copies the pointer

Grid owns a rows-by-cols table of doubles in one block. new double[n]() allocates n doubles; the empty () zero-fills them. Its destructor is complete as written: free the array member.

class Grid {
public:
    Grid(std::size_t r, std::size_t c)
        : cells_(new double[r * c]()), rows_(r), cols_(c) {}
    ~Grid() { delete[] cells_; }
    void set(std::size_t r, std::size_t c, double v) { cells_[r * cols_ + c] = v; }
    double get(std::size_t r, std::size_t c) const { return cells_[r * cols_ + c]; }
private:
    double* cells_;
    std::size_t rows_, cols_;
};

Grid a(1, 2);
a.set(0, 0, 1.5);
Grid b = a;                    // compiler-written copy
b.set(0, 0, 9.0);
std::cout << "a(0,0) = " << a.get(0, 0) << std::endl;
a(0,0) = 9

Then a double-free abort prints on stderr (wording varies). The compiler's copy copies each member in order, so cells_ copies as an address: a and b share one block, and both destructors ran delete[] on it.

The copy constructor and copy assignment

A copy constructor takes const Grid& (a by-value parameter would need a copy itself) and builds its own block:

Grid(const Grid& src)
    : cells_(new double[src.rows_ * src.cols_]), rows_(src.rows_), cols_(src.cols_) {
    std::copy(src.cells_, src.cells_ + rows_ * cols_, cells_);
}

std::copy(first, last, dest) copies the range [first, last) into the elements starting at dest; pointers serve as iterators (the half-open range from the pointers lesson).

Copy assignment, operator=, overwrites an object that already exists (c = a;). Inside a member function, this points at the object it runs on and &src is the argument's address. In order:

  1. If this == &src, return. Otherwise a = a; frees the block it is about to copy from, which is undefined behaviour: it may read back as zeros, old values, or crash.
  2. delete[] the block c owns now, or it leaks.
  3. Allocate a block for the source's size, which can differ.
  4. Update every size member (here rows_ and cols_).
  5. std::copy the elements.
  6. return *this; so x = y = z chains.

With c a 2x3 grid and a a 1x2 grid holding 1.5 2.5, c = a; then c.set(0, 0, 9.0) leaves a as 1.5 2.5 and c as 9 2.5. Freeing before allocating has a cost: if new fails, c has already lost its old block. Fixing that (copy-and-swap) is for later.

What the compiler still writes

The destructor, copy constructor, copy assignment and the move pair (next lesson) are the five special members; the Rule of Five says a class needing one for a raw resource almost always needs all five. Declaring one changes what the compiler writes for the others. A "still written" copy is the member-wise one (each member in order), so with a raw pointer it is the shallow copy again:

You declareOther copy operationMove pair
destructorstill writtennot written
copy constructorcopy assignment still writtennot written
copy assignmentcopy constructor still writtennot written

With no moves, every temporary is copied, allocating an array each time; next lesson fixes that.

Rule of Zero, = default, = delete

The cheapest fix is to write none of it: a std::vector<double> cells_ member makes the compiler's copy correct, because the vector copies its own elements. That is the Rule of Zero; write the five only when you hold a raw resource yourself.

= default asks for the compiler's version of a member; = delete removes it, so using it will not compile. Use it for locks and file handles, which must not be duplicated (delete operator= too).

Common mistakes

  • Only the destructor. Grid b = a; shares one block: changing b changes a, then a double free aborts.
  • Assignment that skips delete[]. Values print correctly, but each assignment leaks a block, so the live count ends above 0.
  • No this == &src guard. a = a; frees the block, then copies from it: garbage or a crash.
  • Forgetting a size member. After assigning a longer buffer to a shorter one the old length remains, so too few values print.

Your exercise

Finish IntBuffer with a destructor, copy constructor and copy assignment. The starter replaces the global operator new[] and operator delete[] with versions that only count blocks, and prints live= from that count, so a leak shows as a number above 0; you never touch it. Copy the elements, not the pointer; the right-hand side can have a different size.

Up nextMove Semantics and Rvalue ReferencesModern C++ Patterns

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…