
Rust `std::mem::swap` and `std::mem::drop`: Controlling Value Movement and Destruction Precisely
Why these functions matter
Rust normally drops values automatically at the end of scope. That is usually ideal, but there are cases where you need more control:
- release a lock before doing expensive work
- replace one value with another without cloning
- move data between two owned locations
- force cleanup before a function returns
- avoid holding a borrow longer than necessary
swap and drop are not glamorous, but they are foundational for writing predictable code in these scenarios.
std::mem::swap: exchanging values in place
swap exchanges the values stored at two mutable locations:
use std::mem;
fn main() {
let mut a = String::from("left");
let mut b = String::from("right");
mem::swap(&mut a, &mut b);
assert_eq!(a, "right");
assert_eq!(b, "left");
}The key detail is that swap works in place. It does not allocate a new String or clone the contents. For large values, this is often much cheaper than copying or rebuilding.
When swap is useful
Common use cases include:
- reordering state in a data structure
- implementing algorithms that exchange buffers
- temporarily moving ownership between two fields
- replacing a value while preserving the old one for later use
A classic example is a double-buffered system:
use std::mem;
struct FrameBuffers {
front: Vec<u8>,
back: Vec<u8>,
}
impl FrameBuffers {
fn present(&mut self) {
mem::swap(&mut self.front, &mut self.back);
}
}This pattern avoids reallocating buffers every frame. The data stays owned, but the roles of the buffers change.
std::mem::drop: ending a value’s lifetime early
drop consumes a value and runs its destructor immediately:
use std::mem;
fn main() {
let file = std::fs::File::open("config.toml").unwrap();
// Do something with the file...
mem::drop(file); // closes the file here
}This is especially useful when a resource should be released before the end of a scope. For example, you might want to unlock a mutex before calling into code that could block or re-enter the same subsystem.
Important behavior
drop takes ownership of its argument. After calling it, the value can no longer be used.
use std::mem;
fn main() {
let s = String::from("hello");
mem::drop(s);
// println!("{}", s); // error: use of moved value
}This is not a special runtime operation; it is just a normal function that consumes the value. The destructor runs as part of that consumption.
Choosing between swap, drop, and related tools
Rust offers several ways to manage values. The right choice depends on your intent.
| Tool | What it does | Best for |
|---|---|---|
std::mem::swap | Exchanges two values in place | Reordering state, buffer rotation, temporary ownership exchange |
std::mem::drop | Consumes a value and runs its destructor immediately | Early cleanup, releasing locks, shortening resource lifetimes |
std::mem::replace | Replaces a value and returns the old one | Moving a value out while leaving a valid replacement |
std::mem::take | Replaces a value with Default::default() and returns the old one | Clearing collections or optional state |
If you need the old value back, drop is the wrong tool. If you need to exchange two values without allocating, swap is usually ideal.
A practical example: releasing a lock before expensive work
One of the most common advanced uses of drop is shortening the lifetime of a lock guard.
use std::sync::{Arc, Mutex};
fn process(shared: Arc<Mutex<Vec<u8>>>) {
let mut guard = shared.lock().unwrap();
guard.push(1);
// Release the lock before expensive computation.
drop(guard);
expensive_work();
}
fn expensive_work() {
// Simulate CPU-heavy work.
let mut sum = 0u64;
for i in 0..1_000_000 {
sum += i;
}
let _ = sum;
}Without the explicit drop, the mutex would remain locked until guard goes out of scope. In small functions that may not matter, but in larger functions it is easy to accidentally hold a lock longer than intended.
Best practice
Use drop sparingly and intentionally. If a value should be released at a clear point, drop can make that obvious. If the scope can be naturally reduced with braces, that is often even clearer:
fn process(shared: Arc<Mutex<Vec<u8>>>) {
{
let mut guard = shared.lock().unwrap();
guard.push(1);
} // guard dropped here
expensive_work();
}Prefer the narrower scope when it keeps the code readable. Use drop when the release point must occur in the middle of a larger scope.
Swapping state in real APIs
swap is especially useful in APIs that maintain internal state across calls. Consider a parser that accumulates output in one buffer while another buffer is being processed.
use std::mem;
struct Parser {
current: String,
staging: String,
}
impl Parser {
fn finish_chunk(&mut self) -> String {
mem::swap(&mut self.current, &mut self.staging);
self.staging.clear();
mem::take(&mut self.current)
}
}This example shows a common pattern: swap to rotate state, then clear or reuse one side. In performance-sensitive code, this avoids repeated allocations and keeps memory usage predictable.
Why not clone?
Cloning large buffers can be expensive and unnecessary. swap lets you move ownership of the underlying allocation, which is often just pointer-sized work. That matters in hot paths, streaming systems, and network services.
Avoiding common mistakes
1. Don’t use drop to “free memory” casually
Calling drop does not necessarily reduce process memory immediately. It runs destructors and releases owned resources, but the allocator may retain memory for reuse. Use it to control lifetime, not to guarantee visible memory reclamation.
2. Don’t expect swap to work without mutable access
Both locations must be mutable:
use std::mem;
fn main() {
let mut x = 1;
let mut y = 2;
mem::swap(&mut x, &mut y);
}This is a feature, not a limitation. It ensures the compiler can enforce exclusive access during the exchange.
3. Don’t call drop on references expecting the referent to disappear
Dropping a reference only drops the reference value, not the thing it points to.
use std::mem;
fn main() {
let s = String::from("hello");
let r = &s;
mem::drop(r); // drops the reference, not the String
println!("{}", s); // still valid
}If you want to destroy the owned value, you must drop the owner.
4. Don’t overuse explicit drop
If a value naturally goes out of scope soon, explicit drop can make code noisier. Reserve it for cases where the timing matters.
swap with collections and nested structures
You can swap fields inside larger structures as long as you have mutable access to both locations.
use std::mem;
struct Cache {
hot: Vec<String>,
cold: Vec<String>,
}
impl Cache {
fn rotate(&mut self) {
mem::swap(&mut self.hot, &mut self.cold);
self.cold.clear();
}
}This is a common technique in caches, queues, and batching systems. It lets you rotate ownership of data without rebuilding the structure from scratch.
If you need to swap elements in a vector, use indexing carefully:
use std::mem;
fn main() {
let mut values = vec![10, 20, 30];
values.swap(0, 2); // Vec has its own efficient swap method
assert_eq!(values, vec![30, 20, 10]);
}For collections, prefer the type’s built-in swap method when available. It often expresses intent more clearly than mem::swap on indexed references.
Design guidance for library authors
When designing APIs, think about whether users need to:
- exchange internal state
- release a resource early
- preserve the old value
- avoid cloning large data
A few practical rules help:
- Use
swapwhen you are rotating or exchanging state. - Use
dropwhen the timing of destruction matters. - Prefer clear scope boundaries when they are enough.
- Document whether a method consumes, replaces, or merely borrows state.
For example, a method named reset might internally use swap or take to preserve old data for logging or reuse. A method named close might explicitly drop a guard or handle to ensure resources are released before returning.
Summary
std::mem::swap and std::mem::drop are simple, but they give you precise control over ownership transitions and destruction timing.
swapexchanges values in place without cloning.dropends a value’s lifetime immediately by consuming it.- Both are most valuable when resource timing and performance matter.
- In many cases, a smaller scope is clearer than an explicit
drop.
Used thoughtfully, these functions help you write Rust code that is efficient, explicit, and easy to reason about.
