
Rust `std::mem::take` and `std::mem::replace`: Moving Values Out of Borrowed Data Safely
Why moving out of &mut T is normally forbidden
Consider a struct that stores a String:
struct User {
name: String,
email: String,
}
fn bad(user: &mut User) -> String {
user.name
}This does not compile because user.name would move the String out of a borrowed struct. The struct would be left partially invalid, and Rust does not allow that by default.
The key idea is that a mutable reference grants permission to modify a value in place, not to dismantle it. If you need to extract a field, you must leave something behind.
That is exactly what take and replace do.
The core difference: take vs replace
Both functions live in std::mem, but they solve slightly different problems.
| Function | What it does | When to use |
|---|---|---|
std::mem::take(&mut value) | Replaces value with Default::default() and returns the old value | When a sensible default exists |
std::mem::replace(&mut value, new_value) | Replaces value with new_value and returns the old value | When you want to choose the replacement explicitly |
take in one line
let old = std::mem::take(&mut some_string);This is shorthand for:
let old = std::mem::replace(&mut some_string, String::default());replace in one line
let old = std::mem::replace(&mut some_string, String::from("new"));This is the more general primitive. take is a convenience wrapper for types that implement Default.
A practical example: draining a field from a struct
Imagine a request context that accumulates logs and then flushes them to disk:
struct RequestContext {
request_id: u64,
logs: Vec<String>,
}
impl RequestContext {
fn flush_logs(&mut self) -> Vec<String> {
std::mem::take(&mut self.logs)
}
}After calling flush_logs, the logs field becomes an empty Vec<String>, and the caller receives the previous contents.
This pattern is useful because it avoids cloning potentially large data. It also keeps the struct in a valid state, ready for reuse.
Why this is better than cloning
If you wrote:
fn flush_logs(&mut self) -> Vec<String> {
self.logs.clone()
}you would keep the logs inside the struct and duplicate all entries. That may be fine for small data, but it is wasteful for large buffers or high-throughput services.
take expresses a different intent: transfer ownership out, leave an empty container behind.
When take is the right tool
take works best when the field has a natural empty state.
Common examples include:
Vec<T>→ empty vectorString→ empty stringOption<T>→NoneHashMap<K, V>→ empty map- custom types that implement
Default
This makes it ideal for “drain and reset” workflows.
Example: taking an Option
struct JobQueue {
current_job: Option<String>,
}
impl JobQueue {
fn start_next(&mut self) -> Option<String> {
std::mem::take(&mut self.current_job)
}
}If current_job contains Some(job), the method returns it and leaves None behind. This is a clean way to model one-shot ownership transfer.
When replace is the right tool
Use replace when the replacement value is not a default, or when the replacement depends on runtime logic.
Example: swapping state during a transition
enum ConnectionState {
Disconnected,
Connected { session_id: String },
}
struct Connection {
state: ConnectionState,
}
impl Connection {
fn disconnect(&mut self) -> ConnectionState {
std::mem::replace(&mut self.state, ConnectionState::Disconnected)
}
}This returns the previous state and resets the connection to Disconnected.
That is especially useful in state machines, where each transition needs to preserve the old state for logging, cleanup, or deferred processing.
Example: replacing with computed data
struct Buffer {
data: String,
}
impl Buffer {
fn swap_contents(&mut self, new_data: String) -> String {
std::mem::replace(&mut self.data, new_data)
}
}Here, take would not work unless an empty string were the desired replacement. replace makes the intent explicit.
A common real-world pattern: extracting from Option<T>
Option<T> is one of the most useful types for these APIs because None is a natural default.
Suppose you are implementing a callback holder:
struct Handler {
on_complete: Option<Box<dyn FnOnce()>>,
}
impl Handler {
fn run(&mut self) {
if let Some(callback) = self.on_complete.take() {
callback();
}
}
}This is a classic pattern for one-time callbacks. The callback is moved out of the struct, leaving None behind so it cannot be called twice.
Why Option::take is so common
Option<T> has a built-in take method that is often more ergonomic than std::mem::take:
let callback = self.on_complete.take();This works because Option<T> can always become None. It is a good example of how Rust’s standard library builds on mem::take-style semantics to support safe extraction.
Choosing between take, replace, and swap
Sometimes you need to move values around but not necessarily extract one and discard the other. In those cases, std::mem::swap may be a better fit.
| Function | Ownership result | Replacement required | Typical use |
|---|---|---|---|
take | Returns old value, leaves default | No, uses Default | Drain a field |
replace | Returns old value, installs chosen value | Yes | State transitions |
swap | Exchanges two values | No | Reordering or temporary exchange |
Example: swapping two fields
struct Pair {
left: String,
right: String,
}
impl Pair {
fn swap_sides(&mut self) {
std::mem::swap(&mut self.left, &mut self.right);
}
}Use swap when both values remain relevant and you simply want to exchange them.
Best practices for using take and replace
1. Prefer the most descriptive operation
If the replacement is a natural default, take is usually clearer than spelling out replace(..., Default::default()).
If the replacement is meaningful state, use replace to make that explicit.
2. Keep the post-condition obvious
A good take-based API makes it easy to answer: “What does this field look like afterward?”
For example:
Vec<T>becomes emptyOption<T>becomesNoneStringbecomes empty
If the replacement state is surprising, document it.
3. Avoid using take to hide ownership complexity
These functions are not a workaround for poor API design. If a method repeatedly needs to extract and reinsert values, consider whether the data structure should be reorganized.
For example, if a type frequently needs to move one field out while preserving invariants across several others, a dedicated helper method may be better than repeated ad hoc take calls.
4. Be careful with invariants
replace is powerful because it lets you install any value, including one that may temporarily violate a higher-level invariant.
For example:
struct Config {
mode: String,
path: String,
}If mode and path must always agree, replacing one field independently may leave the struct in an invalid intermediate state. In such cases, prefer a method that updates the whole struct at once.
A state-machine example with cleanup
A practical use case is a parser or protocol handler that needs to transition between states while preserving the old one for cleanup.
enum ParserState {
Idle,
Reading { buffer: Vec<u8> },
Finished,
}
struct Parser {
state: ParserState,
}
impl Parser {
fn finish(&mut self) -> ParserState {
std::mem::replace(&mut self.state, ParserState::Finished)
}
}This lets you:
- Take ownership of the current state.
- Move the parser into a known terminal state.
- Perform cleanup or logging based on the old state.
That pattern is especially useful when the old state contains buffers, file handles, or partially processed data that must be inspected before being dropped.
Performance and ergonomics considerations
take and replace are zero-cost in the sense that they do not allocate by themselves. Their cost depends on the type being moved.
For String, Vec<T>, and Box<T>, moving the value is cheap because the heap allocation is not copied; only the owning pointer metadata is transferred. For large plain-old-data structs, the cost is copying the struct bytes into the return value and writing the replacement value in place.
A few practical notes:
takeis often the simplest choice for buffers and accumulators.replaceis ideal when the new value is computed as part of the operation.- Avoid unnecessary
clone()calls when ownership transfer is sufficient. - If you only need to inspect a value, borrow it instead of taking it.
Common mistakes
Trying to use take on a type without Default
std::mem::take requires Default. If your type does not have a sensible default, use replace instead.
Forgetting that take mutates the original value
After take, the original location is not uninitialized; it contains the default value. That matters if later code assumes the field still holds meaningful data.
Using replace without considering invariants
Replacing a field is safe at the memory level, but your application logic may still require stronger guarantees. Keep transitions localized and well named.
Summary
std::mem::take and std::mem::replace are essential tools for working with ownership in Rust when you need to move a value out of borrowed data.
Use take when:
- the type has a natural default,
- you want to drain a field,
- and leaving an empty value behind is correct.
Use replace when:
- the replacement is explicit,
- the new value depends on runtime logic,
- or the type does not implement
Default.
Together, they let you write APIs that are both ergonomic and safe, without resorting to cloning or unsafe code.
