Skip to content

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

Hard Primitives

Implement copying and moving for an owned handle

An integer handle can identify a resource without owning it. If a wrapper closes that resource in its destructor, simply copying the integer creates two wrappers that both try to close the same resource.

The Rule of Five asks you to consider the destructor, copy constructor, copy assignment, move constructor and move assignment together. It does not require every wrapper to support copying. A file or socket wrapper might deliberately delete its copy operations. This exercise instead requires a copyable Handle whose copies own independent resources with equal values.

The supplied API

The capi namespace simulates resource management:

  • open_handle(value) creates a resource and returns a positive integer ID.
  • handle_value(id) reads a live resource's value.
  • close_handle(id) marks a live resource closed.
  • open_count() counts resources still open.

Reading or closing an invalid or already-closed ID increments capi::errors. ID 0 is reserved for an empty wrapper and must never be passed to those read/close operations. The simulated API uses value -1 internally to mark closed slots, so exercise inputs exclude -1. Values, IDs and counts fit int.

Your task

Complete Handle while leaving capi and exercise unchanged. Use id_ == 0 as the empty state and implement these behaviors:

Operation Required behavior
Destructor Close the owned resource once, or do nothing if empty.
Copy constructor Open a new resource with the source's value; copying an empty source stays empty.
Copy assignment Replace the destination with an independent copy; preserve its old resource if acquiring the copy throws.
Move constructor Transfer the ID without opening a resource, then empty the source.
Move assignment Release the destination's current resource, transfer the ID and empty the source; self-move leaves the object unchanged.

Copy assignment must also handle self-assignment safely. An empty Handle must remain safe to destroy, copy from, move from and assign to. value() is for non-empty Handles; the supplied exercise never reads a moved-from wrapper. Empty-source assignment should release any old destination resource and leave the destination empty.

Acquire a copy before releasing the old resource

Copy-and-swap is one way to implement copy assignment:

Handle& operator=(const Handle& other) {
    Handle tmp(other);  // may throw before this object changes
    swap(tmp);          // exchange IDs without throwing
    return *this;       // tmp now owns and releases the old resource
}

If acquisition fails, the destination still owns its original resource. For self-assignment, the temporary first owns a separate equal-valued resource; after swapping, it destroys the original resource. This preserves the value without requiring both wrappers to own the same ID.

Make these moves noexcept

This wrapper's move operations only transfer integer IDs and, for assignment, call the non-throwing close_handle. Mark them noexcept because those operations do not throw for supported inputs.

During vector growth, a potentially throwing move can cause existing elements to be copied when copying is available, helping preserve the vector on failure. A noncopyable type may still need to be moved even if moving can throw, with weaker guarantees in that failure case. Do not add noexcept to an operation that can actually let an exception escape just to encourage moving.

What exercise returns

The supplied function grows a vector of Handles, reads their values, then copies the first Handle, moves that copy, and move-assigns it over the last Handle. For non-empty input, its output is the original values followed by the first value three more times.

For [10, 20, 30], the harness reports:

[[10, 20, 30, 10, 10, 10], 0, 0]

The last two entries are the number of resources still open and the API error count, measured after exercise returns. Both must be zero. Empty input returns [[], 0, 0]; zero and repeated values are valid inputs.

The starter's destructor suppresses implicit moves, while its generated copies duplicate IDs. That breaks exclusive ownership. Correct the class's ownership operations rather than altering the API counters or the exercise sequence. The provided sequence does not call copy assignment or self-assignment, so check those required operations separately as well.