We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership I: Moves, Copy, Clone, Drop step 15 of 20
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
ifpoisons 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 markerformat!("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 back —
t0 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…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.