Skip to content

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

Medium Primitives

Moving out of &mut: take, replace, swap

Two exercises in the same technique: moving a value out of a place you only have a &mut to.

pub fn rotate3(a: String, b: String, c: String) -> Vec<String>
pub fn drive(ops: Vec<String>) -> Vec<String>

The problem this solves

Everything so far has been a rule. This is the first genuinely non-obvious technique, and it is the answer to a whole family of complaints that sound like the compiler is being unreasonable:

error[E0507]: cannot move out of `self.pending` which is behind a mutable reference
  |
  |         let items = self.pending;
  |                     ^^^^^^^^^^^^ move occurs because `self.pending` has type
  |                                  `Vec<String>`, which does not implement `Copy`

The rule being enforced is one you already know: a place must always hold a valid value. You do not own self; you have borrowed it, and you must give it back intact. Moving the Vec out would leave a hole in something the caller still owns.

Which points straight at the solution. If the objection is “you would leave a hole”, then do not leave a hole — put something else there. That is all these three functions do:

std::mem::swap(&mut a, &mut b)         // exchange the contents of two places
std::mem::replace(&mut place, new)     // put `new` in, get the old one out
std::mem::take(&mut place)             // replace(&mut place, Default::default())

None of them allocate. swap is three memcpys of the value’s stack bytes; take on a Vec writes the three words of an empty Vec, which has a dangling pointer, zero length and zero capacity and touches no heap at all. This is how real Rust implements iterators, state machines and linked structures without cloning, and it is why Option::take from two problems ago worked.

Part 1: rotate3

Return [c, a, b] — each string moved one place to the right, wrapping — using only mem::swap. Two swaps are enough.

The starter does not compile:

error[E0596]: cannot borrow `a` as mutable, as it is not declared as mutable

&mut x requires x to be a mut binding, and function parameters are bindings like any other. fn rotate3(mut a: String, ...) fixes it. Note that mut on a parameter is entirely a statement about the local binding: the caller gave you the value and does not care what you do with it.

::: question Work out where each value is after each swap. Start from a = A, b = B, c = C.

start                a = A   b = B   c = C
swap(&mut a, &mut b) a = B   b = A   c = C
swap(&mut a, &mut c) a = C   b = A   c = B

and vec![a, b, c] is [C, A, B]. Correct.

Worth noticing what mem::swap is not: it is not let t = a; a = b; b = t;. That sequence needs a temporary that owns a for an instant, which is fine for locals but impossible when the places are behind references — you would be moving out of a borrow to build t. swap never has an intermediate owner; it exchanges the bytes in place, which is why it works on &mut and why it is the primitive the other two are built from. :::

Part 2: drive

struct Buffer {
    pending: Vec<String>,
    flushed: usize,
}

Write Buffer::flush(&mut self) -> String: it must empty pending, return its contents joined with ",", and increment flushed. Then drive runs a script:

  • "push <text>" — append <text> to pending
  • "flush" — call flush and record what it returned
  • anything else — ignore

The return value is every flush output in order, followed by one final line

format!("flushes={}|pending={}", buf.flushed, buf.pending.len())

reporting how many flushes happened and how many items are still pending. So ["push a", "push b", "flush"] gives ["a,b", "flushes=1|pending=0"], and ["push a", "flush", "flush"] gives ["a", "", "flushes=2|pending=0"].

That second one is the assertion that matters: the trailing flush emits the empty string, which proves pending was genuinely emptied by the first flush rather than merely read. And pending=0 in the summary proves the same thing from the other side.

The lint that teaches the idiom

Write the flush like this and it compiles perfectly:

let items = std::mem::replace(&mut self.pending, Vec::new());

and clippy refuses it:

error: replacing a value of type `T` with `T::default()` is better expressed
       using `std::mem::take`
       help: consider using: `std::mem::take(&mut self.pending)`

mem_replace_with_default is on by default, so a learner who reaches for the longhand is told the short form exists, by name, at the moment they need it. There is no better way to learn a standard-library idiom than being handed it the first time you almost need it.

Its siblings are worth knowing too: mem_replace_option_with_none (use .take()), mem_replace_option_with_some (use .replace(v)), and the correctness-level mem_replace_with_uninit, which catches genuinely unsound code.

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

Loading visualization…