Skip to content

← Copy, Move, and the Rule of Zero step 4 of 5

Hard Primitives

Owning something raw costs you five functions

The rule of zero holds until you have to wrap a C API — a file descriptor, a socket, an OpenGL texture, a handle from a library that predates the standard library. Then somebody has to write the destructor, and the moment you do, the compiler’s memberwise defaults are wrong.

Wrong in a specific way. Your member is an int handle, and the generated copy constructor copies the int. Now two objects hold the same handle, both destructors run, and the resource is closed twice. That is a double free with a different noun.

The rule of five: if you write any of the five special members, write all five.

Because writing one changes what you get for the rest — a destructor suppresses the moves, as the previous problem showed — the safe position is to be explicit about the whole set.

What each one has to do

Obligation
Destructor Release what you hold — and only if you still hold it.
Copy constructor Acquire a second resource. Duplicating the handle is the bug.
Copy assignment Release yours, take a copy of theirs, and survive x = x.
Move constructor Take theirs, and leave the source empty so its destructor does nothing.
Move assignment Release yours, take theirs, empty the source, survive self-move.

The two lines that people leave out are the last halves of the move operations. A move that steals the handle and leaves the source holding it too has not moved anything — it has duplicated it, and you are back to the double close.

noexcept is not decoration

Move operations should be marked noexcept, and here is the concrete reason: when a std::vector grows, it must relocate its elements. If the move constructor is noexcept it moves them. If it is not, the vector copies them instead — because it cannot offer the strong exception guarantee any other way. An un-noexcept move constructor silently turns every reallocation back into a deep copy. performance-noexcept-move-constructor is in the gate for exactly this.

Copy-and-swap, briefly

A tidy way to write copy assignment:

Handle& operator=(const Handle& other) {
    Handle tmp(other);       // may throw — nothing has changed yet
    swap(tmp);               // cannot throw
    return *this;            // tmp's destructor releases the old resource
}

Self-assignment works without a check, and it gives the strong guarantee: if the copy throws, *this is untouched. Track 2 promised you would meet this — here it is.

Your task

A C-style API is given: open_handle, handle_value, close_handle. It records an error every time it is handed a handle that is closed or was never valid, and it reports how many handles are still open.

Write Handle so that it wraps one of these correctly. exercise is given and does the things that break a half-written wrapper: it fills a vector that reallocates, copies, moves, and move-assigns.

The starter is the version everyone writes first — a constructor and a destructor, and nothing else. The harness reports the values, the handles left open, and the API’s error count. All three must come out right.