When you need to move data without moving data
You are building a request handler. The handler struct holds a buffer of incoming bytes. When the request finishes, you want to hand that buffer to the response logic. You try to move the buffer out of the handler, but the handler needs to stay alive for the next request. The compiler blocks you with E0507 (cannot move out of borrowed content). You can't move a field out of a struct unless the whole struct is dropped or you use a specific trick.
You also have two large arrays you need to exchange. Copying the bytes is slow. You need to swap the pointers.
std::mem is the toolbox for these memory-level maneuvers. It lets you manipulate values in place, check sizes, control lifetimes, and handle uninitialized memory without fighting the borrow checker or sacrificing performance.
The mechanic's toolkit
Think of your program as a warehouse. Most of the time, you are moving crates around: taking a crate, opening it, putting it back. That's normal logic. std::mem is the mechanic's toolkit for the warehouse structure itself. You use it to swap the labels on two crates without touching the contents. You use it to swap a full crate for an empty box and keep the old crate. You use it to check the blueprint to see how much shelf space a crate type takes. You use it to tell the warehouse manager to ignore a crate so it doesn't get recycled.
These operations are low-level. They touch the memory layout directly. They are safe to use, but they require understanding what the compiler is doing under the hood.
Swap and replace: exchanging values in place
The most common operations are swap and replace. They let you exchange values without copying data.
use std::mem;
/// Swaps two values and replaces a value with a placeholder.
fn main() {
let mut a = String::from("hello");
let mut b = String::from("world");
// Swap exchanges the values in place.
// For String, this swaps the pointer, length, and capacity.
// No characters are copied. This is O(1).
mem::swap(&mut a, &mut b);
println!("a: {}, b: {}", a, b);
// Replace takes a mutable reference, puts a new value,
// and returns the old value.
// This is useful when you need the old value but the
// slot must remain valid.
let old_a = mem::replace(&mut a, String::new());
println!("old_a: {}, a: {}", old_a, a);
}
swap requires two mutable references. It exchanges the values. For types like String, Vec, or Box, the value is just a pointer, length, and capacity. Swapping those three words is instant. The heap data stays where it is. The pointers just change targets.
replace is more flexible. It takes a mutable reference to a slot, writes a new value into that slot, and returns the value that was there. This is the key to extracting data from a struct without destroying the struct.
Don't copy bytes. Swap pointers.
The take idiom: extracting from Option
A common pattern is extracting a value from an Option or Vec inside a struct. You want the value, but you want the struct to stay valid. mem::replace works, but there is a cleaner convention.
struct Request {
id: u32,
body: Option<Vec<u8>>,
}
/// Extracts the body from the request, leaving None behind.
fn process(mut req: Request) {
// mem::take replaces the value with Default::default().
// For Option, the default is None.
// This extracts the Vec without moving out of req.
let body = mem::take(&mut req.body);
// req.body is now None.
// req is still valid and can be reused.
println!("Processing request {} with body size {}", req.id, body.as_ref().map_or(0, |v| v.len()));
}
mem::take is a function that calls replace with Default::default(). For Option<T>, the default is None. For Vec<T>, the default is an empty vector. The community convention is to use take when the default value is the natural placeholder. It signals intent: you are taking the value and leaving the slot in its default state.
Using take is clearer than replace with None. Readers recognize the pattern immediately.
Extract without destroying the container.
Checking sizes: layout and heap
std::mem::size_of tells you how many bytes a type takes in memory. This is a compile-time constant. The compiler calculates it based on the type definition.
use std::mem;
fn main() {
// size_of is a compile-time constant.
// String is a fat pointer: ptr, len, cap.
// On 64-bit systems, that's 8 + 8 + 8 = 24 bytes.
let string_size = mem::size_of::<String>();
println!("Size of String: {} bytes", string_size);
// Vec is the same layout as String.
let vec_size = mem::size_of::<Vec<u8>>();
println!("Size of Vec<u8>: {} bytes", vec_size);
// i32 is 4 bytes.
let int_size = mem::size_of::<i32>();
println!("Size of i32: {} bytes", int_size);
}
size_of only measures the type itself. It does not include heap data. A String with a million characters is still 24 bytes according to size_of. The characters live on the heap. size_of measures the handle, not the payload.
For slices and dynamically sized types, the size is not known at compile time. You need size_of_val.
use std::mem;
fn main() {
let slice: &[u8] = &[1, 2, 3, 4, 5];
// size_of::<[u8]>() fails because the size is unknown.
// size_of_val measures the actual data behind the reference.
let data_size = mem::size_of_val(slice);
println!("Slice data size: {} bytes", data_size);
// A slice reference itself is 16 bytes: ptr + len.
let ref_size = mem::size_of::<&[u8]>();
println!("Slice reference size: {} bytes", ref_size);
}
size_of_val takes a reference and returns the size of the value it points to. This is essential for working with slices, strings, and trait objects.
Know your layout. Heap data doesn't count.
Controlling lifetimes: drop and forget
Rust drops values automatically when they go out of scope. Sometimes you need to control that timing. drop lets you drop a value early.
use std::mem;
fn main() {
let mut vec = vec![1, 2, 3];
// Create a borrow.
let borrow = &vec[0];
// This would fail: cannot borrow vec as mutable because
// it is also borrowed as immutable.
// vec.push(4);
// drop ends the borrow early.
mem::drop(borrow);
// Now the borrow is gone. We can mutate vec.
vec.push(4);
println!("{:?}", vec);
}
drop is a function that takes ownership and drops the value. It's often used to end a borrow when the borrow is no longer needed but the variable is still in scope.
mem::forget does the opposite. It prevents a value from being dropped. This leaks memory.
use std::mem;
fn main() {
let data = String::from("leaked");
// forget prevents the String from being dropped.
// The heap memory is leaked.
mem::forget(data);
// data is gone. We can't use it.
// The memory is lost until the program exits.
}
forget is dangerous. It's used in unsafe abstractions where you take ownership of memory and manage it manually. For most code, avoid forget. If you need to suppress a drop, use ManuallyDrop instead. It makes the intent explicit and keeps the value accessible.
Trust the borrow checker. It usually has a point. If you need forget, you are likely building a safe abstraction.
Uninitialized memory: MaybeUninit
Sometimes you need to allocate memory without initializing it. This happens in performance-critical code or when building data structures. std::mem::MaybeUninit is the safe way to handle uninitialized memory.
use std::mem::MaybeUninit;
/// Writes to uninitialized memory safely.
fn main() {
// Allocate space for a String without initializing it.
// This avoids running the String constructor.
let mut slot = MaybeUninit::<String>::uninit();
// write places a value into the slot without dropping
// the old value. There is no old value, so this is safe.
// This is useful when you want to construct a value
// in place.
unsafe {
// SAFETY: slot is uninitialized. write does not
// drop the old value. We are writing a valid String.
slot.write(String::from("hello"));
}
// assume_init tells the compiler the value is initialized.
// This is unsafe because the compiler trusts you.
// If the value is not initialized, this is undefined behavior.
let s = unsafe {
// SAFETY: We wrote a String into slot above.
// The value is fully initialized.
slot.assume_init()
};
println!("{}", s);
}
MaybeUninit wraps a value that might not be initialized. write places a value without dropping the old one. assume_init extracts the value. Both are unsafe because you must ensure the memory is valid.
MaybeUninit replaces transmute in most cases. It's the standard way to handle uninitialized memory in Rust.
Transmute is a lie to the compiler. If you can't prove the lie is safe, don't tell it.
Pitfalls and compiler errors
std::mem functions are powerful, but they have traps.
If you try to move a field out of a struct, the compiler rejects you with E0507 (cannot move out of borrowed content). This happens when you try to extract a non-Copy value from a struct that needs to stay alive. Use mem::replace or mem::take to extract the value and leave a placeholder.
If you call size_of on a slice or trait object, the compiler rejects you with E0071 (cannot apply unary operator). The size is not known at compile time. Use size_of_val instead.
If you use transmute, you risk undefined behavior. transmute tells the compiler to treat one type as another. If the layouts don't match or the bit patterns are invalid, the program crashes or corrupts memory. Avoid transmute. Use MaybeUninit, ptr::write, or safe conversions instead.
If you call assume_init on uninitialized memory, you get undefined behavior. The compiler assumes the value is valid. If it's not, anything can happen. Always ensure the value is initialized before calling assume_init.
Don't fight the compiler here. Reach for MaybeUninit.
Decision: when to use std::mem functions
Use mem::swap when you need to exchange two values without copying data. Use mem::replace when you need to extract a value from a slot but must leave a placeholder to keep the struct valid. Use mem::take when extracting from an Option or Vec and the default value is the natural placeholder. Use mem::size_of when you need the layout size of a type at compile time. Use mem::size_of_val when dealing with slices or dynamically sized types where the size is only known at runtime. Use drop when you need to end a borrow early or force cleanup of a value. Use MaybeUninit when you need to allocate memory without initializing it or write a value without dropping the old one. Avoid mem::transmute unless you are implementing a safe abstraction and have audited the layout. Reach for MaybeUninit or ptr::write instead.
Counter-intuitive but true: the more you use unsafe, the harder the rest of your code becomes to reason about.