Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 15 of 20

Hard Primitives

Conditional moves and drop flags

Drop some values early, keep the rest, and get the order exactly right.

pub fn conditional_drops(flags: Vec<bool>) -> Vec<String>

Ownership is not purely a compile-time fiction

Almost. Consider:

let t = make_thing();
if condition {
    consume(t);
}
// end of scope — should `t` be dropped here or not?

Whether t still needs destroying depends on a runtime condition. The compiler cannot know, so it emits a drop flag: a hidden boolean, set when the value is initialised and cleared when it is moved out, checked at the end of the scope. It is one byte on the stack and one branch, and -O usually removes it entirely once the optimiser can prove which way the condition went.

Three things you have already met are consequences of drop flags:

  • “value moved here, in previous iteration of loop” — the flag would have to be cleared on iteration 1 and set again before iteration 2, and nothing sets it.
  • “used binding is possibly-uninitialized” — the flag says “no value here” on at least one path reaching this use.
  • Moving inside an if poisons the variable afterwards, even on the path where the move did not happen. The compiler is conservative on purpose: it requires a value to be in the same state on every path that merges, so if one branch moves, the merge point treats it as moved.

That last one is not a limitation of the analysis. It is what makes the analysis cheap and predictable, and it is why the fix is always the same shape: decide what happens to the value inside the branch, and do not let the merge point inherit a question.

::: question Can you observe the drop flag from your program? No, and it is worth being honest about that rather than pretending the exercise below is showing you the machinery.

You cannot take its address, name it, or read it. Under -O — which is how this harness compiles everything — the optimiser usually deletes it, because once inlining has happened the branch condition is often known. What you can observe is the consequence: which destructors ran, and in what order. That is what the log below is.

So the honest statement is: a drop flag is a real runtime artefact, its cost is approximately zero, and the only evidence you will ever have of it is the behaviour it produces. If an explanation of drop flags promises you can watch one, it is overclaiming. :::

What to build

The grader injects the usual scaffolding — tracked(name, log) logging "new <name>", a Drop impl logging "drop <name>", note(log, line), new_log(), snapshot(log) — plus a fixed driver you should leave alone:

pub fn conditional_drops(flags: Vec<bool>) -> Vec<String> {
    let log = new_log();
    run(flags, log.clone());
    snapshot(log)
}

The driver exists so that run can return early and still have all of its locals destroyed before the log is read. Write run:

Phase 1. For each flag, in order, build a value named t{i} (so t0, t1, …). If the flag is true, keep it in a Vec of survivors. If it is false, drop it immediately.

Phase 2. Let kept be the number of survivors.

  • If kept == 0: emit the marker "no banner" and leave the function.
  • Otherwise: build one more value named banner{kept}, and emit the marker format!("ready {}", banner.name).

Phase 3. Nothing. The function ends; the compiler cleans up.

For flags = [true, false, true] the log is

new t0, new t1, drop t1, new t2, new banner2, ready banner2,
drop banner2, drop t0, drop t2

Two orders are being asserted at once in that tail. banner is a plain local declared after the survivors Vec, so it dies first — locals go in reverse. Then the Vec dies and releases its elements front to backt0 before t2. Get one of them backwards and the test will tell you which.

The two errors in the starter

error[E0382]: use of moved value: `t`
  |     if !flag { drop(t); }
  |                     - value moved here
  |     survivors.push(t);
  |                    ^ value used here after move

The move happens on one path and the use happens on all of them.

error[E0381]: used binding `banner` is possibly-uninitialized

let banner: Tracked; followed by an assignment in only one branch of an if/else, and then a read. On the other path there is nothing there.

About needless_late_init

Deferred initialisation — let x; now, x = ... later — is legal Rust and occasionally the only way to express something. It is also, most of the time, a habit imported from languages where declarations must come first, and clippy will say so:

error: unneeded late initialization
help: move the declaration `s` here

needless_late_init is on by default, and it fires on both of the obvious shapes: assigning in every branch of an if/else, and declaring early then assigning once after a guard clause. Which means the clean way through phase 2 is the one clippy is pushing you toward — put the guard clause first, return out of it, and then declare banner normally where you build it. Late init is worth reaching for when the assignment happens inside a loop or under a condition clippy cannot fold away; here it is not.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…