Skip to content

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

Easy Primitives

Shadowing is not moving

Five small scenes about two operations that look identical and are not.

pub fn shadow_timeline(scene: i64) -> Vec<String>

Two things that both reuse a name

let t = make("a");
let t = make("b");     // shadowing: a NEW binding that happens to reuse the name

let mut u = make("c");
u = make("d");         // assignment: the SAME binding, given a new value

Learners routinely believe the first one destroys "a". It does not. Shadowing creates a second binding; the first one still exists, still owns its value, and is simply no longer reachable by that name. Both die at the end of the scope, in reverse declaration order, exactly like any two locals:

new a, new b, end of body, drop b, drop a

Assignment is the operation that destroys. Writing into a slot that already holds a value must first get rid of what is there, or the old value’s destructor would never run and its memory would leak. So u = make("d") drops "c" — and it does it at the assignment point, not at the end of the scope.

::: question In u = make("d"), does "c" drop before or after "d" is built? After. The order is:

  1. evaluate the right-hand side, producing "d"
  2. drop the old occupant of the slot, "c"
  3. move "d" into the slot

so the log reads new d, then drop c. Predicting this backwards is one of the most common small errors people make when reasoning about a Drop type, and it matters in real code: if the value is a lock guard or a file handle, “the new one is acquired before the old one is released” is a very different program from “the old one is released first”.

The reason for the order is straightforward once you ask what happens if step 1 panics. If the old value had already been dropped, the slot would hold garbage and the unwinder would have to know not to drop it again. Building first keeps the slot valid at every instant. :::

E0384: assignment needs mut

Shadowing needs no mut, because each let is a fresh binding. Assignment does:

error[E0384]: cannot assign twice to immutable variable `u`
  |
  |     let u = tracked("c", log.clone());
  |         - first assignment to `u`
  |     u = tracked("d", log.clone());
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
help: consider making this binding mutable: `mut u`

The starter contains that error. Fix it, then build the remaining scenes.

The scaffolding

Injected by the grader:

fn tracked(name: String, log: Log) -> Tracked   // pushes "new <name>"
impl Drop for Tracked                           // pushes "drop <name>"
fn note(log: Log, line: String)                 // pushes an arbitrary line
fn new_log() -> Log
fn snapshot(log: Log) -> Vec<String>

Tracked has a name: String field you can read.

The five scenes

Scene 0 is written for you.

0 — plain shadowing. Two locals both called _t, holding a then b; then the marker "end of body".

1 — assignment. One mut local u, holding c. Emit format!("holding {}", u.name). Assign d into u. Emit format!("holding {}", u.name) again.

2 — shadowing in an inner block. Local t holding e; emit format!("outer {}", t.name); then a block containing a shadowing local t holding f and the marker format!("inner {}", t.name); after the block, emit format!("back to {}", t.name).

3 — shadowing in a loop. For i in 0..2: a local t holding g{i}, the marker format!("first {}", t.name), a shadowing local t holding h{i}, and the marker format!("iter {}", t.name).

4 — assignment in a loop. A mut local u holding z0. For i in 1..3: emit format!("holding {}", u.name), then assign z{i} into u. After the loop, emit format!("final {}", u.name).

Scene 2 is the one that shows shadowing is scoped: after the inner brace, the name t refers to the outer value again, and the outer value was never touched. Scene 3 shows that a loop body is a scope like any other — both shadows die every iteration.

One lint to know about

There is a tempting sixth scene — the “re-bind by move” idiom let x = x;, which is genuinely useful for forcing a move into a closure or narrowing mutability. It is not here, because clippy’s redundant_locals fires on it:

error: redundant redefinition of a binding `x`

redundant_locals is a suspicious-level lint, on by default at warn, which -D warnings promotes to an error. So the idiom would fail this problem’s gate through no fault of yours. Worth knowing it exists, and worth knowing why you cannot practise it here.

The shadow_same, shadow_reuse and shadow_unrelated lints, by contrast, are all in the restriction group and off by default. Shadowing is idiomatic Rust; nothing in the default configuration discourages it.

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

Loading visualization…