Skip to content

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

Easy Primitives

Reading E0382 line by line

Three broken helpers, three different flavours of the same error. Repair all three without touching the driver.

pub fn run(inputs: Vec<String>) -> Vec<String>

You will see E0382 more often than every other Rust error combined. This problem is about diagnostic literacy: the claim that rustc’s message contains, every single time, four specific facts, and that once you know where to look for them the compiler stops being an adversary.

The four facts in every E0382

error[E0382]: use of moved value: `word`                    <- (1) which variable
  |
  |     format!("{}:{}", tag(word), width(word))
  |                          ----          ^^^^ (3) used here after move
  |                          |
  |                          (2) value moved here
  |
  = note: move occurs because `word` has type `String`,
          which does not implement the `Copy` trait          <- (4) why it moved
  1. The variable. Named, not described.
  2. The move site. The exact expression that took ownership.
  3. The use site. Where you asked for it afterwards.
  4. The reason. Always the same reason — the type is not Copy — but rustc says it every time because it is the fact you are most likely to have forgotten.

Everything that follows in the message is advice, and advice is optional. The four facts are not.

This structure generalises. E0505 (“cannot move out of X because it is borrowed”), E0507 (“cannot move out of behind a reference”) and E0509 (“cannot move out of a type that implements Drop”) all name the value, the conflicting event, the site, and the reason. You are learning a format, not a special case.

The three flavours

(a) Moved into a consuming helper, then used again.

fn stage_a(word: String) -> String {
    format!("{}:{}", tag(word), width(word))
}

Both tag and width take String by value. The second call is the use site.

(b) Moved inside a for body.

fn stage_b(items: Vec<String>) -> Vec<String> {
    let acc: Vec<String> = Vec::new();
    for item in items {
        absorb(acc, item);
    }
    acc
}

Here the message reads “value moved here, in previous iteration of loop”. The first iteration is fine. The second has nothing to pass.

(c) Moved in one arm of an if, then used after.

fn stage_c(word: String, shout: bool) -> String {
    let head = if shout { loud(word) } else { String::from("-") };
    format!("{head}/{word}")
}

Note the wording changes to “borrow of moved value”: format! only needs to read word, so the conflict is a borrow rather than a second move. The rule is the same — a moved-from variable supports no operations at all — but the phrase is different, and reading the difference tells you what the use site was doing.

About “help: consider cloning”

rustc will offer .clone() for all three. It is good advice roughly a third of the time, and this problem is calibrated so you can feel where the line is.

  • In (a), cloning is genuinely the answer under these fixed signatures. One extra allocation buys you a value two consumers can each have.
  • In (c), cloning is fine too, and only happens on the branch that needs it.
  • In (b), cloning is absurd — and, more importantly, wrong. Cloning acc each turn gives the helper a throwaway copy, so the real accumulator never grows and the function returns an empty vector while compiling perfectly. The compiler cannot know that; only the tests can.

::: question What does (b) actually want, if not a clone? absorb has the signature fn absorb(acc: Vec<String>, item: String) -> Vec<String> — it takes the accumulator and gives it back. The loop needs to catch it:

let mut acc: Vec<String> = Vec::new();
for item in items {
    acc = absorb(acc, item);
}

That is the same “move in, move out, same slot” pattern as the previous problem, and it is what Iterator::fold does under the hood. Recognising when a consuming function returns the thing it consumed — and therefore does not need a clone — is most of the skill of not writing clone-happy Rust. :::

What the driver does

run is already correct; leave it alone. For a list of inputs it produces, in order: stage_a applied to each input, then all of stage_b‘s output, then stage_c applied to each input with shout = true at even indices and false at odd ones. The helpers are:

fn tag(s: String) -> String   { format!("[{s}]") }
fn width(s: String) -> usize  { s.chars().count() }
fn loud(s: String) -> String  { s.to_uppercase() }
fn absorb(mut acc: Vec<String>, item: String) -> Vec<String> {
    let n = acc.len();
    acc.push(format!("{n}={item}"));
    acc
}

None of their signatures may change. Note width counts chars, not bytes.

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

Loading visualization…