Skip to content

← Errors step 3 of 4

Medium Primitives

Declare non-throwing Buffer moves

A noexcept declaration promises that an exception will not escape a function:

void step() noexcept;

The body may still call operations that throw, provided it handles their exceptions internally. If an exception attempts to cross the noexcept boundary, std::terminate is called instead of passing it to a caller's catch block. In C++20, whether unwinding happens fully, partly or not at all before that termination is implementation-defined. Do not rely on cleanup in that situation.

The compiler does not prove that the whole body can never throw. It can diagnose some mistakes, but the declaration remains a contract you must justify.

Why move declarations affect vector growth

When a vector needs more capacity, it relocates existing elements. A throwing move could change some source elements before a later move fails. Copying can preserve the originals until relocation succeeds.

For a copyable type such as Buffer, standard libraries commonly use copies when moving may throw, and moves when the move constructor is noexcept. A type that cannot be copied may still be moved even if moving can throw; that failure can have weaker guarantees. Thus, potentially throwing moves do not universally imply copying.

Buffer's missing noexcept declaration makes its move constructor appear potentially throwing even though the operations used here do not throw for supported inputs. That can cause avoidable Buffer copies during growth.

Check the actual operations

For this Buffer, data is std::vector<int> with the default allocator. Its ordinary move constructor and move assignment are non-throwing. The counter increments remain within int's range in the tests. Both Buffer move operations can therefore promise noexcept.

Do not generalize this to every vector overload or allocator. Allocator-aware move construction and move assignment can have different requirements. Copying a vector may allocate and throw, so Buffer's copy operations should remain potentially throwing.

More generally, declare noexcept when the function's contract and implementation support it. A function that allocates can still honor the promise if it handles allocation failures internally. Destructors are usually implicitly non-throwing, but their exception specifications can depend on bases and members. Neither noexcept nor its absence is a substitute for reviewing the operations involved.

Your task

Add noexcept to Buffer's move constructor and move assignment operator. Keep their bodies, the copy operations, data member and counters unchanged. Leave the supplied grow function unchanged:

std::vector<Buffer> grow(std::vector<Buffer>& buffers);

grow moves each input Buffer into a new vector without reserving capacity. This intentionally allows growth to exercise relocation. Reallocation uses move construction, not move assignment; declare both moves correctly even though this loop does not test assignment directly.

The destination must retain every buffer's data in order, including empty buffers. The source vector keeps its number of Buffer objects, whose data members have been moved from; the task does not require retaining their original contents in the source.

What the harness reports

It resets the counters after building inputs, then returns [sizes, copies]. sizes contains the length of each destination buffer, in order. For [[1], [2, 2], [3, 3, 3]], expect [[1, 2, 3], 0]. Empty input produces [[], 0]; three empty buffers produce [[0, 0, 0], 0].

The required copy count is zero in the exercise runtime. The harness tracks moves internally but does not return or assert their count, since vector capacity growth differs between implementations. Its output checks lengths, so also check that your unchanged move bodies preserve the actual values. Zero Buffer copies does not mean zero allocations: the destination vector still needs storage.