&mut, and what to do instead;std::mem::swap exchanges two values in place with no clone and no temporary;std::mem::replace swaps in a new value and hands you the old one back;std::mem::take moves a value out of a &mut, leaving its Default behind;mem::forget fits in.Default (episode 22) and Drop (episodes 20 and 42);Learn Rust Series):The std::mem module is a small box of tools that all answer one recurring ownership puzzle: how do you move a value out of a place you only have mutable access to -- a struct field behind &mut self, a slot in a slice, a variable someone lent you -- without cloning it? You cannot just move it out and walk away, because that would leave a hole where the type system insists a valid value must remain. swap, replace and take all solve this the same clever way: they put something valid back into the hole at the exact moment they lift the old value out. Today we take those three apart, use them to write a genuinely clean state machine, and then meet their strange cousin forget, which does the opposite of everything and is useful precisely because of it ;-)
Last episode closed on a promise: the forget/ManuallyDrop/ptr::read dance we used to hand a value back out of our hand-built Rc was pointing straight at "the small standard-library toolkit for moving values into and out of places safely". This is that toolkit. Having said that, let us clear the homework first.
Episode 42 was about drop order, the drop check, and leak safety. Three exercises, and here is full, runnable code for each.
Exercise 1 asked you to make a struct with three fields that each print their name in Drop, give the struct no Drop impl of its own, predict the print order, then reorder the fields and confirm the output follows the new declaration order. Fields drop top to bottom:
struct Loud(&'static str);
impl Drop for Loud {
fn drop(&mut self) { println!("dropping {}", self.0); }
}
struct Bundle {
a: Loud,
b: Loud,
c: Loud,
}
fn main() {
let _bundle = Bundle {
a: Loud("a"),
b: Loud("b"),
c: Loud("c"),
};
// No Drop impl on Bundle, so fields drop in DECLARATION order: a, b, c.
// Swap the field order in the struct definition and the output follows it.
}
The key insight: with no Drop impl on the container, there is no "value's own destructor runs first" step to worry about -- the fields simply tear down in the order they are declared, top to bottom. Reorder the a, b, c fields in the struct definition and the printed order changes to match, deterministically, every time.
Exercise 2 wanted a borrowing struct Holder<'a>(&'a String) whose Drop reads the reference, with a main where the borrowed String is declared after the Holder so it would drop first -- and asked you to read the exact drop-check error. Here is the version that fails, exactly as intended:
struct Holder<'a>(&'a String);
impl<'a> Drop for Holder<'a> {
fn drop(&mut self) {
println!("holding '{}' as I drop", self.0);
}
}
fn main() {
let holder; // declared first -> dropped LAST
let name = String::from("data"); // declared second -> dropped FIRST
holder = Holder(&name); // ERROR: `name` does not live long enough
}
The compiler stops you with `name` does not live long enough. Because holder is declared before name, rule 1 says holder drops last -- but its destructor still wants to read &name, which has already been freed by then. That is a use-after-free waiting to happen, and the drop check refuses it. Swap the two let lines so name is declared first (and therefore dropped last) and it compiles cleanly.
Exercise 3 asked you to hold two resources in ManuallyDrop and release them in the opposite order from their declaration, then, as a second step, mem::forget a third resource and confirm its destructor never runs:
use std::mem::{self, ManuallyDrop};
struct Res(&'static str);
impl Drop for Res {
fn drop(&mut self) { println!("releasing {}", self.0); }
}
fn main() {
let mut a = ManuallyDrop::new(Res("A"));
let mut b = ManuallyDrop::new(Res("B"));
// Normally A (declared first) would drop LAST. We choose the order ourselves:
unsafe {
ManuallyDrop::drop(&mut b); // releasing B -- first
ManuallyDrop::drop(&mut a); // releasing A -- second
}
let c = Res("C");
mem::forget(c); // destructor SKIPPED: "releasing C" never prints
println!("done");
}
ManuallyDrop suppresses the automatic drop, so nothing fires at the closing brace -- we fire the destructors by hand, B before A, inverting the normal reverse-declaration order. Then mem::forget(c) throws away resource C without running its destructor at all, so you never see releasing C. Those two escape hatches -- deciding when a destructor runs, and deciding whether it runs -- are exactly the mindset we need for today. Right, homework cleared. Now the tools.
Start with the wall everybody hits. You have a &mut to something, and you want the value it points at, by value. Rust says no:
struct Config { name: String }
fn steal_name(c: &mut Config) -> String {
c.name // ERROR: cannot move out of `c.name` which is behind a mutable reference
}
fn main() {}
The error is cannot move out of ... which is behind a mutable reference, and it is not the compiler being fussy for the sake of it. A &mut is a borrow: you are holding someone else's value temporarily, and you have promised to give it back intact. If you were allowed to move c.name out, the Config behind the reference would be left with a name field that holds nothing -- a hole -- and the moment its true owner touched that field again, or the moment it got dropped, you would have a read of uninitialised memory. Rust closes that door at compile time.
But the underlying operation is completely legitimate: sometimes you really do need the owned value out of that field. The trick, and the whole idea behind std::mem, is that you are allowed to move a value out as long as you put a valid one back in the same breath. Leave no hole, and the borrow contract is honoured. That is what the next three functions do.
mem::swap takes two mutable references and exchanges their contents. No clone, no temporary variable, no Copy bound -- it works on any type at all, including big owned things like String and Vec:
use std::mem;
fn main() {
let mut a = String::from("first");
let mut b = String::from("second");
mem::swap(&mut a, &mut b);
println!("a = {a}, b = {b}"); // a = second, b = first
}
Both a and b stay valid throughout -- they simply trade contents. Under the hood this is a byte-for-byte swap of the two values' representations, which is why no clone and no allocation happen: the two String headers (pointer, length, capacity) are exchanged, and the heap buffers they point at never move. swap is the primitive; replace and take are both built on top of it.
Where swap shines on its own is anywhere you would otherwise fight the borrow checker over two mutable slots at once. A tiny undo buffer is a clean example -- swapping the current value with the saved previous one:
use std::mem;
struct Editor {
current: String,
previous: String,
}
impl Editor {
fn undo(&mut self) {
mem::swap(&mut self.current, &mut self.previous);
}
}
fn main() {
let mut ed = Editor {
current: String::from("version two"),
previous: String::from("version one"),
};
ed.undo();
println!("current = {}", ed.current); // version one
println!("previous = {}", ed.previous); // version two
}
Two owned Strings change places without a single clone, and calling undo twice puts everything back -- a swap is its own inverse. Notice we never had to name a temporary or clone anything; swap did the whole exchange in place.
mem::replace is swap with a fresh value on one side. You give it a mutable reference and a brand-new value; it drops the new value into place and returns the old value to you, by value. This is the direct, literal answer to "take the old value out of a &mut and leave a valid one behind":
use std::mem;
fn main() {
let mut config = vec![1, 2, 3];
let old = mem::replace(&mut config, vec![4, 5, 6]);
println!("old: {old:?}"); // [1, 2, 3]
println!("new: {config:?}"); // [4, 5, 6]
}
The old vector is moved out and handed back; config now holds the new one; nothing was cloned, and there was never a moment where config held a hole. Remember the steal_name function that would not compile? Here is the version that works, because it leaves a valid String behind:
use std::mem;
struct Config { name: String }
fn steal_name(c: &mut Config) -> String {
mem::replace(&mut c.name, String::new()) // hand back the old name, leave an empty one
}
fn main() {
let mut c = Config { name: String::from("production") };
let taken = steal_name(&mut c);
println!("taken: {taken}"); // production
println!("left: '{}'", c.name); // '' (empty, but valid)
}
mem::replace(&mut c.name, String::new()) pulls the real name out and drops an empty String into the field. The borrow contract is satisfied -- the field is never invalid -- and the caller gets the owned String they wanted. That is the pattern in its purest form.
Very often the "valid value to leave behind" is just the type's default -- an empty Vec, an empty String, a None, a zero. mem::take is exactly replace where the replacement is T::default(). It moves the current value out and leaves the default in its place, so it only works for types that implement Default (which, conveniently, is most of the ones you would want to drain). The canonical use is emptying a buffer through &mut self:
use std::mem;
struct Buffer { data: Vec<u8> }
impl Buffer {
fn drain(&mut self) -> Vec<u8> {
mem::take(&mut self.data) // returns the data, leaves an empty Vec behind
}
}
fn main() {
let mut buf = Buffer { data: vec![1, 2, 3] };
let taken = buf.drain();
println!("taken: {taken:?}"); // [1, 2, 3]
println!("remaining: {:?}", buf.data); // []
}
drain lifts the whole vector out through &mut self, and the field is left as an empty Vec -- its default -- so the struct stays perfectly valid and can be filled again. No clone, no allocation for the empty default (an empty Vec does not allocate), and the caller owns the data outright.
take is especially idiomatic with Option, where the default is None. Pulling a one-shot value out of a field and leaving None behind is a two-word operation:
use std::mem;
struct Session { token: Option<String> }
impl Session {
fn take_token(&mut self) -> Option<String> {
mem::take(&mut self.token) // leaves None behind
}
}
fn main() {
let mut s = Session { token: Some(String::from("abc123")) };
println!("first: {:?}", s.take_token()); // Some("abc123")
println!("second: {:?}", s.take_token()); // None -- already taken
println!("field: {:?}", s.token); // None
}
The first call yields the token and swaps in None; every call after that yields None, because that is what now lives in the field. This "consume once, leave None" shape is everywhere in real Rust -- Option::take on the standard library is literally mem::take specialised, and you will reach for it constantly.
Here is where these tools earn their keep, and it is genuinely one of the prettiest patterns in idiomatic Rust. You have a state machine stored behind a &mut, and to compute the next state you need to consume the current one by value -- because the transition owns the data it carries. But you only have a mutable borrow, so a plain match *state that tries to move the payload out will not compile (same wall as before). mem::replace cuts straight through it:
use std::mem;
#[derive(Debug)]
enum State {
Idle,
Running(String),
}
fn stop(state: &mut State) -> Option<String> {
match mem::replace(state, State::Idle) {
State::Running(job) => Some(job), // we OWN `job` now
State::Idle => None,
}
}
fn main() {
let mut state = State::Running(String::from("build"));
println!("{:?}", stop(&mut state)); // Some("build")
println!("{:?}", stop(&mut state)); // None -- already Idle
println!("{:?}", state); // Idle
}
Trace what happens: mem::replace(state, State::Idle) writes Idle into state and returns the old State::Running(job) by value. Because that returned enum is ours now, not borrowed, we can pattern-match it and pull the owned String out. Trying to match *state directly and move job out would fail with "cannot move out of ... behind a mutable reference" -- the exact error we started the episode with. Replacing the state with a valid placeholder (Idle) first is what makes the move legal. This "replace with a cheap placeholder, then consume the old value" move is the backbone of state machines, parsers, and any type that transitions between variants that carry data.
The module also holds mem::forget, and it is the black sheep of the family. Where swap, replace and take are careful never to leave a hole, forget deliberately walks away from a value without running its destructor and without freeing anything it owns:
use std::mem;
struct Handle;
impl Drop for Handle {
fn drop(&mut self) { println!("closed"); }
}
fn main() {
let h = Handle;
mem::forget(h); // destructor skipped -- "closed" never prints
println!("done");
}
Run it and you see only done. Handle's destructor never fires. As we discussed at length last episode, this is perfectly safe in Rust's precise sense of the word -- failing to clean up cannot corrupt memory or hand you a dangling pointer, it can only leak. On its own, an accidental forget is just a memory leak, which is why you almost never want it in ordinary code.
So why does it exist? Because it is the essential partner for raw-memory surgery. Recall the try_unwrap we wrote last episode: we used ptr::read to move a value out of an allocation, then mem::forget(self) to stop our own Drop from freeing that same allocation a second time, and only then freed it by hand. Without forget, that double-free would be unavoidable. Its cousin ManuallyDrop (episode 42) is usually the cleaner modern choice for the same job, because it makes the intent local and visible, but plain forget is the primitive underneath. The rule of thumb: if you find yourself reaching for forget in everyday application code, stop -- there is almost always a cleaner design (a Vec you should have drained, a value you should have returned). It belongs in the internals of smart pointers and collections, not in your business logic.
Since quite some of you arrived here from the Learn Python Series, a sideways glance sharpens the picture -- because this whole "move out of a place" problem is one that only shows up once a language takes ownership seriously.
In Python, you never think about any of this. Names are just references to objects, and "swapping" is the famous one-liner a, b = b, a, which quietly rebinds two names to the same two objects -- no move semantics, no borrow to fight, because nothing is ever exclusively owned in the way a Rust value is. Emptying a field is self.data = [] and letting the garbage collector deal with whatever the old list's refcount does next. There is no mem::replace because there is no borrow checker demanding you leave a valid value behind -- Python is happy to let a name point at None, or at nothing meaningful, and only complains when you actually use it.
In C++, the story is much closer to Rust, and in fact Rust's mem::swap and mem::replace are direct descendants of std::swap and std::exchange. A moved-from C++ object is left in a "valid but unspecified" state -- the language convention is that you may assign to it or destroy it but not much else. Rust makes that discipline mandatory and machine-checked: a value moved out through mem::take is not left "unspecified", it is left as a concrete, fully valid Default. Where C++ trusts you to remember not to read a moved-from object, Rust's borrow checker simply will not let a hole exist in the first place. Same engineering instinct, far stronger guarantee.
In Go, there is no move semantics at all -- everything is copied or shared through the garbage collector -- so the "move out of a place" puzzle never arises. You swap two variables with a, b = b, a, exactly like Python, and you empty a slice by reassigning s = nil. The cost is that Go cannot express the thing Rust's mem module is built to express: transferring unique ownership of a heavy resource with zero copying and a compiler guarantee that the old location is never left dangling. Different trade, different language.
One shared truth across all four: swapping and draining are trivial in a garbage-collected world and subtle in an ownership-based one. Rust's std::mem is the small, sharp toolkit that makes them trivial again, without giving up the guarantees -- which is a fair summary of the language as a whole.
&mut and leave a hole -- the borrow contract forbids it. The whole std::mem toolkit exists to move a value out while putting a valid one back in the same instant.mem::swap(&mut a, &mut b) exchanges two values in place, no clone, no Copy bound, works on any type. It is the primitive the others build on, and it is its own inverse.mem::replace(&mut place, new) drops new into place and returns the old value by value -- the literal answer to "take the old value out of a &mut".mem::take(&mut place) is replace with T::default() as the new value: it moves the current value out and leaves the default (empty Vec, empty String, None). Requires T: Default.mem::replace(state, Placeholder) to consume the current variant by value -- the clean way to transition a state machine held behind &mut.mem::forget is the odd one out: it skips a value's destructor without freeing what it owns. Safe (a leak, not corruption), rare, and mostly a building block for unsafe code that takes over cleanup manually -- prefer ManuallyDrop when you can.Three exercises, gentle to chewier. Type them out and run them -- the "leave a valid value behind" idea only really clicks once you have watched a field survive having its contents lifted out from under it.
mem::swap to rotate three variables so that a gets b's value, b gets c's, and c gets a's, using exactly two swaps. Print all three before and after to confirm the rotation.Option<String> field and write a method that uses mem::take to pull the value out, leaving None. Call it twice and show that the second call returns None.Red, Green, Yellow) and write fn next(&mut self) that uses mem::replace to consume the old state, compute the next colour from it, and write the new colour back -- without ever cloning or fighting the borrow checker.That mem::replace-to-consume-a-variant trick, and the way it lets a function reach through a mutable borrow to move an owned value out, is quietly leaning on some subtle machinery around how generic functions accept borrowed data of any lifetime. That machinery is where we head next. Chew on it ;-)
Bedankt en tot de volgende keer! ;-)