Skip to content

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

Hard Primitives

Conditional moves and drop flags

Create tracked values, drop some immediately, and keep others until a final cleanup phase. Return the complete creation and destruction log.

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

Each flag controls one value. Process flags in input order, including repeated values. The index starts at zero and becomes part of the name: t0, t1, and so on.

Helpers supplied by the harness

Helper Behavior
new_log() Creates a shared, initially empty Log.
tracked(name, log) Appends new <name> and returns a Tracked value whose name field your code can read.
Tracked's destructor Appends drop <name>.
note(log, line) Appends the supplied line unchanged.
snapshot(log) Returns a copy of the log entries.

Cloning a Log shares the same log; it does not clone a tracked value. The starter already provides the conditional_drops driver. Keep it and implement run(flags, log). The driver reads the log after run returns, so destruction performed inside run is included in the result.

Required sequence

1. Process the flags. For each index i, create t{i}. If its flag is true, move the value into a vector of survivors. Otherwise, drop it before processing the next flag.

2. Prepare the banner. Count the survivors. If there are none, append no banner and return. This also applies to empty input. Otherwise, create a value named banner{count}, then append ready banner{count}.

3. Clean up in a specified order. Drop the banner first. Then consume the survivors in input order, dropping each one before advancing to the next. Use an explicit loop to establish this order: Vec does not guarantee its element destruction order when the vector itself is dropped.

For flags = [true, false, true], return these separate strings:

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

The exercise is about ownership and cleanup, so use the supplied tracked values to produce the log. The harness checks the returned entries rather than inspecting your implementation.

Why the starter fails

In phase 1, the starter calls drop(t) when the flag is false, then tries to push t into the survivors on every path. drop takes ownership, so the false path has already moved the value. This produces E0382, a use of a moved value. Put the two outcomes in separate branches: either keep t or drop it.

The banner has a different problem. The starter declares banner without initializing it, assigns it only when survivors exist, then reads its name on both paths. E0381 reports that the binding may be uninitialized. Return from the empty-survivor case before declaring and using the banner.

These errors concern whether a value is definitely available at a use. Cleanup must also handle paths on which a value has already moved or was never initialized, but a runtime drop flag does not make an invalid use legal.

What a drop flag represents

The compiler may need runtime state to decide whether a value still needs destruction. A drop flag records that initialization state. Where the state is known statically, cleanup can be unconditional or omitted. Exact storage, branches and optimization depend on the generated program; there is no fixed one-byte cost or guarantee that optimization removes all flags.

The log here observes destructor behavior, not the compiler's chosen representation. Drop flags are not named Rust variables your program can read. Compiler intermediate output or generated code can help investigate their implementation, but those details are not a stable program interface.

A conditional move does not permanently invalidate a binding. You can initialize it again before a later use, or make the path that moved it leave the function. This complete example shows both approaches:

fn replace_if_needed(replace: bool) -> String {
    let mut value = String::from("original");
    if replace {
        drop(value);
        value = String::from("replacement");
    }
    value
}

fn keep_unless(discard: bool) -> Option<String> {
    let value = String::from("kept");
    if discard {
        drop(value);
        return None;
    }
    Some(value)
}

fn main() {
    assert_eq!(replace_if_needed(false), "original");
    assert_eq!(replace_if_needed(true), "replacement");
    assert_eq!(keep_unless(false).as_deref(), Some("kept"));
    assert_eq!(keep_unless(true), None);
}

Keep initialization close to use

Deferred initialization is legal when every path reaching a use supplies a value. It can be helpful when control flow needs it. Clippy's needless_late_init identifies some cases that can instead use a declaration at the assignment or an expression-valued if.

Here, a guard clause makes the banner's lifetime clear: return when there are no survivors, then construct it normally. After the ready marker, explicitly drop the banner and then each survivor. Submissions must compile, produce the expected logs and pass the runner's Clippy checks.