RFA-673 · Case file with fixtures · Case 645 of 694 · Runtime evidence
mem::take Replaces State With Its Default Value
mem::take moves a value out by leaving Default behind. Confirm that Default is a valid temporary state, or use replace with an explicit successor.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Moving a value out through a mutable reference requires a valid replacement, and take deliberately chooses the type's Default value.
- First discriminating check
- Confirm Default is valid on every early-exit path or use replace to install an explicit successor that preserves the outer invariant.
std::mem::take(&mut place) returns the current value and writes T::default() into the place. The failing fixture takes a state at generation seven and finds generation zero in the original location.
take is replace with Default
The mem::take contract is equivalent in purpose to replacing with a default value. It is concise for strings, vectors, options, and structs where an empty or baseline value is valid.
This permits moving a field out through a mutable borrow. Safe Rust requires the field to remain initialised before the surrounding object can be used or dropped. Default supplies that valid replacement.
I read take as an ownership transition, not a borrow. The returned value is independent owned state and the source has changed.
Default must be a valid state here
The Default trait provides a useful starting value, but it does not universally mean empty, inactive, or semantically neutral. A derived numeric field becomes zero. An enum's selected default may represent a real mode.
If taking a field temporarily leaves an object whose methods assume a nonzero generation, authenticated session, or non-empty schema, the surrounding invariant is broken even though the type is valid.
I use take when the default is acceptable for every point at which the outer object can be observed. Otherwise mem::replace installs an explicit successor. The repaired fixture moves out generation seven while installing generation eight.
Empty collection reuse is a strong use case
Taking a Vec<T> returns its elements and leaves a new default empty vector. This may not preserve the original allocation in the source; ownership of the allocation usually travels with the returned vector. If the goal is to process elements while retaining capacity, drain, clear, or swapping with a prepared buffer may be more suitable.
Taking a String similarly transfers its buffer. This is often exactly what parsers and builders need, but performance code should measure allocations rather than assume “empty” means capacity remains.
For Option<T>, option.take() leaves None, which expresses the state better and avoids mentioning mem.
Borrow scope still matters
After taking a field, code owns the old value and may mutate other fields. This is useful in state transitions that would otherwise create overlapping mutable borrows.
The outer object remains borrowed for the duration of the original &mut access, but normal method calls often let the compiler shorten that borrow. I keep the transition in a small block and restore any cross-field invariant before returning control.
If processing the taken value can fail, I decide whether to restore it, keep the default, or install a failure state. A ? after take can return early and leave the default behind. Sometimes this is correct; sometimes it causes quiet data loss.
Panic and cancellation expose temporary state
A panic between take and restoration drops the taken owner during unwinding and leaves the outer object's default field for its destructor. In asynchronous code, an await between those points also permits cancellation while the default is installed.
I avoid suspension and fallible calls inside a temporarily weakened invariant. A guard whose destructor restores state can help, or a consuming state-machine method can make intermediate states unobservable.
Tests force error and panic paths, not only successful restoration. They assert the source postcondition and capacity behaviour when performance depends on it.
My take checklist
- Is
T::default()valid in this exact field and phase? - Does the caller understand that the source changes immediately?
- Should an explicit successor be installed with replace instead?
- Does taking a collection transfer capacity that was meant to be reused?
- Can
?, panic, or async cancellation occur before restoration? - Are cross-field invariants temporarily broken?
- Would a specialised
Option::takeor collection method be clearer? - Do tests cover failure while the default is installed?
The core principle is that moving out through a borrow requires leaving a complete value behind. mem::take chooses Default for that value. I use it only when the default is an honest state, including on every early-exit path.