Learn Rust Series (#43) - std::mem: swap, replace, take & forget

What will I learn
- You will learn why you cannot simply move a value out of a
&mut, and what to do instead; - how
std::mem::swapexchanges two values in place with no clone and no temporary; - how
std::mem::replaceswaps in a new value and hands you the old one back; - how
std::mem::takemoves a value out of a&mut, leaving itsDefaultbehind; - how these three power clean state-machine transitions and draining patterns, and where the odd cousin
mem::forgetfits in.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Rust toolchain (via rustup, from rustup.rs);
- The previous forty-two episodes, especially ownership (episode 3),
Default(episode 22) andDrop(episodes 20 and 42); - The ambition to learn systems programming from the ground up.
Difficulty
- Intermediate
Curriculum (of the Learn Rust Series):
- Learn Rust Series (#1) - Introduction to Rust
- Learn Rust Series (#2) - Variables, Types, Functions
- Learn Rust Series (#3) - Ownership & Borrowing
- Learn Rust Series (#4) - Control Flow & Pattern Matching
- Learn Rust Series (#5) - Structs & Enums
- Learn Rust Series (#6) - Error Handling
- Learn Rust Series (#7) - Collections
- Learn Rust Series (#8) - Traits & Generics
- Learn Rust Series (#9) - Modules & Crates
- Learn Rust Series (#10) - Lifetimes
- Learn Rust Series (#11) - Closures & the Iterator Trait
- Learn Rust Series (#12) - Smart Pointers: Box, Rc & RefCell
- Learn Rust Series (#13) - Concurrency: Threads, Channels, Arc & Mutex
- Learn Rust Series (#14) - Mini Project: A Command-Line To-Do App
- Learn Rust Series (#15) - Trait Objects & Dynamic Dispatch
- Learn Rust Series (#16) - Static vs Dynamic Dispatch
- Learn Rust Series (#17) - Associated Types vs Generic Parameters
- Learn Rust Series (#18) - Operator Overloading with std::ops
- Learn Rust Series (#19) - Deref, DerefMut & Deref Coercion
- Learn Rust Series (#20) - Drop & Deterministic Destruction (RAII)
- Learn Rust Series (#21) - From, Into, TryFrom & Idiomatic Conversions
- Learn Rust Series (#22) - Deriving Common Traits
- Learn Rust Series (#23) - The Orphan Rule & Trait Coherence
- Learn Rust Series (#24) - Blanket Implementations & the Newtype Pattern
- Learn Rust Series (#25) - Marker Traits: Sized, Send, Sync & Copy
- Learn Rust Series (#26) - Const Generics: Types That Depend on Values
- Learn Rust Series (#27) - Generic Associated Types & Lending Iterators
- Learn Rust Series (#28) - Sealed Traits & Designing Stable APIs
- Learn Rust Series (#29) - Typestate Programming: State Machines in the Type System
- Learn Rust Series (#30) - Mini Project: A Generic Units-of-Measure Library
- Learn Rust Series (#31) - Move Semantics Deep Dive
- Learn Rust Series (#32) - Interior Mutability: Cell & RefCell
- Learn Rust Series (#33) - Rc Internals: Reference Counting & Shared Ownership
- Learn Rust Series (#34) - Arc: Thread-Safe Reference Counting & Its Cost
- Learn Rust Series (#35) - Weak References & Breaking Reference Cycles
- Learn Rust Series (#36) - Cow: Clone-on-Write for Borrow-or-Own APIs
- Learn Rust Series (#37) - Pin & Self-Referential Structs
- Learn Rust Series (#38) - PhantomData, Zero-Sized Types & Marker Lifetimes
- Learn Rust Series (#39) - Variance: Covariance, Contravariance & Why It Matters
- Learn Rust Series (#40) - Arena & Bump Allocation Patterns
- Learn Rust Series (#41) - Building Your Own Smart Pointer
- Learn Rust Series (#42) - Drop Order, the Drop Check & Leak Safety
- Learn Rust Series (#43) - std::mem: swap, replace, take & forget (this post)
Learn Rust Series (#43) - std::mem: swap, replace, take & forget
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.
Solutions to Episode 42 Exercises
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.
Why you cannot just move out of a &mut
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.
swap: exchange two values in place
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.
replace: swap in the new, get out the old
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.
take: move out, leave the default
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.
The state-machine pattern
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 odd one out: forget
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.
How this compares to other languages
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.
What did we actually learn?
- You cannot move a value out of a
&mutand leave a hole -- the borrow contract forbids it. The wholestd::memtoolkit 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, noCopybound, works on any type. It is the primitive the others build on, and it is its own inverse.mem::replace(&mut place, new)dropsnewintoplaceand returns the old value by value -- the literal answer to "take the old value out of a&mut".mem::take(&mut place)isreplacewithT::default()as the new value: it moves the current value out and leaves the default (emptyVec, emptyString,None). RequiresT: Default.- The state-machine pattern uses
mem::replace(state, Placeholder)to consume the current variant by value -- the clean way to transition a state machine held behind&mut. mem::forgetis 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 forunsafecode that takes over cleanup manually -- preferManuallyDropwhen you can.
Exercises
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.
- Use
mem::swapto rotate three variables so thatagetsb's value,bgetsc's, andcgetsa's, using exactly two swaps. Print all three before and after to confirm the rotation. - Give a struct an
Option<String>field and write a method that usesmem::taketo pull the value out, leavingNone. Call it twice and show that the second call returnsNone. - Model a traffic light as an enum (
Red,Green,Yellow) and writefn next(&mut self)that usesmem::replaceto 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! ;-)
@scipio


