Skip to content

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

Medium Primitives

What is left after you move from it

In a language with destructive moves, moving out of a variable ends it — the compiler refuses to let you touch it again. C++ does not do that. The destructor still has to run, so the object must still be there, and the language settles for a weaker promise:

A moved-from object is in a valid but unspecified state.

Valid means the two things a destructor needs: it can be destroyed, and it can be assigned to. Every other question — what it contains, how long it is, whether it is empty — has no answer you are entitled to.

std::vector<int> b = std::move(a);
a.size();     // legal to call, and you may not rely on the answer
a.clear();    // fine — no precondition
a = other;    // fine — assignment is how you give it a value again
a[0];         // undefined behaviour — has a precondition you cannot check

The distinction is precondition, not danger. size() promises nothing about its result but is safe to call. operator[] requires an element to exist, and you have no way to know whether one does.

Where this actually bites

It is almost never std::move(a); use(a); on adjacent lines. It is:

  • a move inside an if, and a read after the if
  • a member moved out in a loop body, and read on the next iteration
  • a variable moved into a lambda, and read in the enclosing function
  • a “just logging it” line added six months later by someone who did not look up

Three types, three different answers

Because implementations vary and the standard does not, the honest summary is short:

Type After being moved from
std::unique_ptr Guaranteed null. The standard says so explicitly.
std::vector, std::string Unspecified. Usually empty, and a short string often is not moved at all.
std::optional Still engagedhas_value() stays true, and the contained value is moved-from.

That optional row surprises people every time, and it is why “moved-from means empty” is not a rule you can carry around.

The rule you can carry:

Do not read a moved-from object. Ask your question before the move, or assign it a fresh value first.

Your task

Batch drain(std::vector<Record>& records);

Each Record holds a std::unique_ptr<std::vector<int>> which may be null. Move every non-null sample list out of records into Batch::collected, in order, and set Batch::with_samples to how many records had one.

The starter moves first and asks afterwards. unique_ptr is the one row of that table where the answer is guaranteed, so the count comes back zero every time — a bug that is fully deterministic here and would be maddening with a std::vector member instead.

Move the question, not the move.