Skip to content

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

Medium Primitives

Restore memberwise moves with the Rule of Zero

A class built from members that already manage their resources can often rely on compiler-generated cleanup, copying and moving. This approach is called the Rule of Zero: avoid declaring custom resource-management operations when the members already provide the behavior the class needs.

The related Rule of Five concerns these operations:

~T();                     // destructor
T(const T&);              // copy constructor
T& operator=(const T&);   // copy assignment
T(T&&);                   // move constructor
T& operator=(T&&);        // move assignment

The default constructor is also a special member function, but is not one of these five. A class does not necessarily get usable versions of all of them. Declarations can suppress generation, and a member can make an operation unavailable. For example, a unique_ptr member prevents ordinary memberwise copying. Generated moves are not unconditionally noexcept; their exception specification depends on the operations they perform.

A defaulted destructor can suppress moves

Consider the starter's declaration:

~Session() = default;

Even though its body is generated, this is a user-declared destructor. It prevents implicit declaration of Session's move constructor and move assignment operator. User-declared copy operations also suppress implicit moves.

In this Session, the copy constructor remains usable. Consequently, std::move(session) can bind to the copy constructor's const reference parameter. std::move changes the expression's value category; it does not create a move operation that the class lacks.

That fallback is specific to types with a suitable copy operation. A class whose copy operation is unavailable may instead fail to compile. Do not assume that every suppressed move silently becomes a copy.

Let the members do the work

Session contains a Counted object, which already implements the copy and move behavior needed here. Removing the unnecessary destructor declaration allows Session's implicit moves to use Counted's move operations. Session still has a destructor, generated by the compiler, that destroys its member.

This approach reduces handwritten member-by-member code. It does not prove that every future class change is safe: adding a pointer into another member, for example, may require reconsidering how copies and moves preserve that relationship. Review the class's invariants whenever its representation changes.

When a custom destructor or copy/move operation is needed, consider all five operations together. Depending on the intended interface, an operation may be defaulted, deleted or implemented. The Rule of Five does not require five handwritten bodies or make every resource-owning class copyable.

Your task

Change only Session by removing its unnecessary destructor declaration. Keep its constructors, accessor and Counted member. Leave Counted, its counters, and collect unchanged:

std::vector<Session> collect(std::vector<Session>& sessions);

The supplied collect reserves enough output capacity, then constructs one output Session from each input using std::move. After your change, it should preserve every event list and its order in the output while making zero Counted copies and exactly one Counted move per Session, including empty Sessions.

The harness resets the counters after building the input and reports [collected_event_lists, copies, moves]. For [[1, 2], [], [3]], expect:

[[[1, 2], [], [3]], 0, 3]

Empty input produces [[], 0, 0]. The source vector keeps its number of Sessions, but their members have been moved from. The task does not require their original event contents to remain there.

Reserving prevents output growth from moving already-collected Sessions again. These counters measure Counted operations, not allocations or every low-level data movement. Returning the output vector does not add a move for each of its elements.