Rust 13 🦀 Move Semantics and Variable Scope Drop Behavior
Move semantics and drop behavior are two sides of the same mechanism. A move transfers ownership from one binding to another, and a drop releases the value when its final owner goes out of scope. Together they define the life cycle of every value in a Rust program: where it starts, how it travels, and when it ends. The compiler tracks this life cycle statically, inserting drops at the right places and rejecting code that would use a value after it has moved or after it has been dropped.
Understanding move semantics requires understanding what actually happens in memory. A move copies the stack representation of a value — the pointer, length, and capacity for a String — and invalidates the source binding. The heap data stays where it is, with a new owner. A drop is the reverse: when the owner goes out of scope, Rust calls the destructor, which frees the heap data. For types without heap data, the drop is a no-op, but the compiler still tracks the scope. The two operations form a complete ownership cycle, and the compiler’s job is to verify that every value has exactly one owner at all times and exactly one drop at the end.
This chapter covers move semantics in detail, partial moves, moves out of collections, the drop order in scopes and structs, the Drop trait, std::mem::drop, and the patterns that follow from understanding these mechanics.
Key point: A move transfers ownership by copying the stack representation and invalidating the source. A drop frees the value when the owner goes out of scope. Drop order is reverse of declaration for locals, and fields are dropped in declaration order for structs. The Drop trait customizes what happens at drop. std::mem::drop drops a value early. Types that implement Drop cannot be Copy.
Why move semantics and drop behavior matter
The single-owner problem. Every value must have exactly one owner at any point in time. A move transfers that ownership, ensuring the source can no longer free the value. Without moves, two bindings could refer to the same heap allocation, and both would try to free it, causing a double-free.
The timely-free problem. Every value must be freed exactly once, at the right time. The drop is the mechanism: when the owner goes out of scope, the value is freed. The compiler inserts the drop at the end of the scope, and it runs deterministically. There is no garbage collector that might delay the free or never run it.
The resource problem. Not all resources are memory. Files, network connections, locks, and database handles must be released. The Drop trait lets a type define what “release” means, and the compiler calls it automatically. This is the RAII (Resource Acquisition Is Initialization) pattern, and Rust implements it uniformly.
The partial-move problem. A struct with multiple fields can have one field moved out while the others remain. The compiler tracks which fields are still valid and rejects access to the moved ones. Understanding partial moves is essential when destructuring structs and working with collections.
The drop-order problem. When multiple values go out of scope at the same time, the order in which they are dropped can matter, especially if the destructors have side effects or if the values depend on each other. Rust specifies the drop order so the behavior is predictable.
a. What a move actually does
A move copies the stack representation of the value and marks the source binding as invalid. The heap data is not copied and does not move.
let s1 = String::from("hello");
let s2 = s1;
Before the move, s1 holds a pointer to the heap, a length, and a capacity. After the move, s2 holds the same pointer, length, and capacity. s1 is invalidated; the compiler will reject any use of it.
The heap data (h, e, l, l, o) is untouched. It stays at the same address, and s2 is now the owner. When s2 goes out of scope, the heap data is freed.
This is why moves are cheap. Copying the stack representation is a fixed-size copy — three words for a String — regardless of how much heap data the value owns.
let big = vec![0u8; 1_000_000];
let moved = big;
// Only the three-word stack representation moved, not the million bytes.
The vector’s million bytes stay on the heap, and the move is constant-time.
b. Partial moves
A move can affect a single field of a struct, leaving the other fields valid. The compiler tracks which fields have been moved.
struct Person {
name: String,
age: u32,
}
let person = Person {
name: String::from("Alice"),
age: 30,
};
let name = person.name; // moves the name field
// person.name is now invalid
// person.age is still valid
println!("{}", person.age); // OK
After moving person.name, the name field is invalid, but age is still accessible because it implements Copy and was not moved. The compiler rejects access to person.name but allows person.age.
A partial move of a non-Copy field is allowed, but using the struct as a whole after a partial move is not:
// println!("{:?}", person); // Error: use of partially moved value
The rule is that once any field is moved out, the struct cannot be used as a whole. Individual unmoved fields remain accessible.
Partial moves are common when destructuring:
let Person { name, age } = person;
// name and age are separate bindings; person is partially moved
The destructuring pattern moves the fields that are bound by value and copies the ones that are Copy.
c. Moves out of collections
A value inside a collection can be moved out, which removes it from the collection.
let mut v = vec![String::from("a"), String::from("b")];
let first = v.remove(0); // moves out and removes
println!("{}", first); // "a"
println!("{:?}", v); // [b]
The remove method takes the element at the given index, removes it from the vector, and returns it. The vector is now one element shorter, and the returned String is owned by first.
Indexing a vector does not move; it borrows:
let first = &v[0]; // borrows, does not move
Attempting to move out by index is a compile error:
// let first = v[0]; // Error: cannot move out of index
The reason is that indexing could panic or leave the vector in an inconsistent state. The explicit remove method makes the removal clear.
Similarly, pop moves the last element out:
let last = v.pop(); // Option<String>
pop returns Option<T> because the vector may be empty.
For a map, remove returns Option<V>:
let mut map = HashMap::new();
map.insert("a", String::from("1"));
let value = map.remove("a"); // Option<String>
All of these methods move the value out of the collection, leaving the collection without that element.
d. Drop order
When a scope ends, the values declared in it are dropped in reverse order of declaration. This is the last-in, first-out rule.
fn main() {
let a = String::from("first");
let b = String::from("second");
let c = String::from("third");
// c drops first, then b, then a
}
The reason is that later values may depend on earlier ones, so dropping in reverse order ensures that dependencies are still valid when a destructor runs.
For structs, fields are dropped in declaration order, not reverse:
struct Pair {
first: String,
second: String,
}
{
let pair = Pair {
first: String::from("1"),
second: String::from("2"),
};
// first drops, then second
}
The struct’s fields are dropped in the order they are declared. The struct’s own destructor runs first, then the fields.
For closures and captured variables, the drop order follows the capture order, which is not always obvious. When the drop order matters, it can be made explicit by dropping values manually with std::mem::drop.
e. The Drop trait
The Drop trait defines what happens when a value is dropped. Implementing it allows a type to release resources or perform cleanup.
struct FileHandle {
name: String,
}
impl Drop for FileHandle {
fn drop(&mut self) {
println!("Closing {}", self.name);
}
}
{
let f = FileHandle { name: String::from("data.txt") };
} // prints "Closing data.txt"
The drop method takes &mut self and is called automatically when the value goes out of scope. It cannot be called directly, and a type cannot implement Copy if it implements Drop, because dropping a copy would free the same resource twice.
The Drop trait is the mechanism behind Rust’s RAII pattern. File handles close themselves, mutex locks release themselves, and Vec frees its heap allocation. All of this is implemented by implementing Drop for those types.
A common use is to ensure that a resource is released even if the code that uses it panics. Because drop runs during stack unwinding, the resource is still released.
impl Drop for LockGuard {
fn drop(&mut self) {
self.lock.release();
}
}
If the code that holds the guard panics, the guard is dropped, and the lock is released. This is the pattern behind MutexGuard in the standard library.
f. Explicit drop with std::mem::drop
Values are dropped automatically at the end of their scope, but sometimes a value should be dropped earlier. std::mem::drop takes a value by value and lets it go out of scope, triggering the destructor.
let s = String::from("hello");
drop(s);
// s is no longer valid
The drop function is not special. It is a regular function that takes a value and does nothing with it, so the value goes out of scope at the end of the function, and its destructor runs.
pub fn drop<T>(_x: T) {}
Because the parameter _x is taken by value, the caller’s binding is moved into the function, and the function drops the value when it returns. This is the mechanism for early drop.
A common use is releasing a lock before the end of a scope:
let guard = mutex.lock().unwrap();
// do work with the lock
drop(guard);
// lock is released; other code can acquire it
Without the explicit drop, the lock would be held until the end of the scope, which might be later than necessary.
Another use is dropping a large value before allocating another, to reduce peak memory:
let data = load_large_data();
process(&data);
drop(data); // free before allocating more
let more = load_more_data();
The explicit drop communicates the intent and reduces the peak memory usage.
Complete Example Session
// ============================================
// PART 1: MOVE ON ASSIGNMENT
// ============================================
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s2);
// println!("{}", s1); // Error: use of moved value
}
// ============================================
// PART 2: MOVE IS CHEAP
// ============================================
fn main() {
let big = vec![0u8; 1_000_000];
let moved = big;
// Only the three-word stack representation moved.
println!("{}", moved.len());
}
// ============================================
// PART 3: PARTIAL MOVE
// ============================================
struct Person {
name: String,
age: u32,
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
let name = person.name; // moves name
// person.name is invalid
println!("{}", person.age); // OK, age is Copy
println!("{}", name);
}
// ============================================
// PART 4: MOVE OUT OF VECTOR
// ============================================
fn main() {
let mut v = vec![String::from("a"), String::from("b")];
let first = v.remove(0);
println!("{}", first); // a
println!("{:?}", v); // [b]
let last = v.pop();
println!("{:?}", last); // Some("b")
}
// ============================================
// PART 5: INDEX BORROWS, NOT MOVES
// ============================================
fn main() {
let v = vec![String::from("a")];
let first = &v[0]; // borrows
println!("{}", first);
// let moved = v[0]; // Error: cannot move out of index
}
// ============================================
// PART 6: DROP ORDER FOR LOCALS
// ============================================
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("Dropping {}", self.0);
}
}
fn main() {
let _a = Noisy("first");
let _b = Noisy("second");
let _c = Noisy("third");
// Drops in reverse: third, second, first
}
// ============================================
// PART 7: DROP ORDER FOR STRUCT FIELDS
// ============================================
struct Pair {
first: Noisy,
second: Noisy,
}
fn main() {
let _pair = Pair {
first: Noisy("first field"),
second: Noisy("second field"),
};
// Drops in declaration order: first field, second field
}
// ============================================
// PART 8: CUSTOM DROP
// ============================================
struct FileHandle {
name: String,
}
impl Drop for FileHandle {
fn drop(&mut self) {
println!("Closing {}", self.name);
}
}
fn main() {
let _f = FileHandle { name: String::from("data.txt") };
println!("File open");
// drops at end of main
}
// ============================================
// PART 9: EXPLICIT DROP
// ============================================
fn main() {
let s = String::from("hello");
println!("Before drop");
drop(s); // drops here
println!("After drop");
// s is no longer valid
}
// ============================================
// PART 10: DROP RELEASES LOCK
// ============================================
use std::sync::Mutex;
fn main() {
let mutex = Mutex::new(0);
{
let mut guard = mutex.lock().unwrap();
*guard += 1;
} // guard drops here, lock released
let value = mutex.lock().unwrap();
println!("{}", *value);
}
These ten parts cover move on assignment, the cheapness of moves, partial moves, moving out of a vector, indexing versus moving, drop order for locals, drop order for struct fields, custom Drop, explicit drop, and lock release via drop.
Quick Reference
Move Semantics
| Context | Behavior |
|---|---|
| Assignment | Source invalidated |
| Function call | Ownership transfers to parameter |
| Return | Ownership transfers to caller |
| Struct field | Field moves out |
Vec::remove | Element moves out |
Vec::pop | Last element moves out |
| Indexing | Borrows, does not move |
Drop Order
| Context | Order |
|---|---|
| Locals in a scope | Reverse of declaration |
| Struct fields | Declaration order |
| Tuple elements | Element order |
| Closure captures | Capture order |
| Function parameters | Not specified |
Drop Trait
| Aspect | Detail |
|---|---|
| Method | fn drop(&mut self) |
| Called | Automatically at scope end |
| Cannot be | Called directly or implemented with Copy |
| Use | Release resources |
Explicit Drop
| Function | Signature |
|---|---|
std::mem::drop | pub fn drop<T>(_x: T) {} |
Partial Moves
| Situation | Result |
|---|---|
| Move one field | Other fields still valid |
| Move multiple fields | Remaining fields still valid |
| Use struct as whole | Error after partial move |
| Destructure | Binds moved and copied fields |
Best Practices
✅ Do This:
// Let moves happen naturally
let s2 = s1;
// Use references when ownership is not needed
fn process(s: &str) { }
// Implement Drop for resources
impl Drop for Connection { fn drop(&mut self) { self.close(); } }
// Drop explicitly to release early
drop(guard);
// Use destructuring for partial moves
let Person { name, age } = person;
❌ Don’t Do This:
// Use a moved value
let s2 = s1;
println!("{}", s1); // ❌
// Move out of an index
let first = v[0]; // ❌
// Implement Drop and Copy
#[derive(Copy, Clone)]
struct R; // ❌ if R implements Drop
// Rely on drop order for side effects
// Explicitly drop when order matters
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Use of moved value | Source was invalidated | Use a reference or clone |
| Cannot move out of index | Indexing borrows | Use remove or swap_remove |
| Partial move error | Struct used as whole | Access only valid fields |
| Drop order surprise | Assumed order | Check reverse declaration order |
| Drop and Copy conflict | Both implemented | Remove Copy |
| Lock held too long | Guard outlives need | Explicit drop |
Real-World Examples
1. Move into Function
fn process(s: String) { }
process(owned);
2. Borrow to Avoid Move
fn process(s: &str) { }
process(&owned);
3. Partial Move
let Person { name, .. } = person;
4. Remove from Vector
let item = v.remove(index);
5. Pop from Vector
let last = v.pop(); // Option<T>
6. Custom Drop
impl Drop for File {
fn drop(&mut self) { self.close(); }
}
7. Explicit Drop
drop(guard); // release lock early
8. Drop in Nested Scope
{
let _lock = mutex.lock().unwrap();
} // released here
9. Drop Order in Struct
struct Pair { a: A, b: B }
// a drops before b
10. Drop on Panic
// Drop runs during unwind
let _guard = LockGuard::new();
panic!("error"); // guard still dropped
Visual
Move Semantics
┌──────────────────────────────────────────────────────────────┐
│ let s1 = String::from("hello"); │
│ let s2 = s1; │
│ │
│ BEFORE MOVE: │
│ ┌──────────────┐ │
│ │ s1: ptr ─────┼─────▶ heap: "hello" │
│ │ len: 5 │ │
│ │ cap: 5 │ │
│ └──────────────┘ │
│ │
│ AFTER MOVE: │
│ ┌──────────────┐ │
│ │ s1: invalid │ │
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ s2: ptr ─────┼─────▶ heap: "hello" │
│ │ len: 5 │ │
│ │ cap: 5 │ │
│ └──────────────┘ │
│ │
│ Only the stack representation moved; the heap is unchanged. │
└──────────────────────────────────────────────────────────────┘
Partial Move
┌──────────────────────────────────────────────────────────────┐
│ struct Person { name: String, age: u32 } │
│ │
│ let person = Person { name: "Alice".into(), age: 30 }; │
│ let name = person.name; │
│ │
│ person.name → moved (invalid) │
│ person.age → still valid (Copy) │
│ person → cannot be used as a whole │
│ │
│ println!("{}", person.age); // OK │
│ println!("{}", person.name); // Error │
│ println!("{:?}", person); // Error │
└──────────────────────────────────────────────────────────────┘
Drop Order
┌──────────────────────────────────────────────────────────────┐
│ LOCALS (reverse declaration): │
│ let a = ...; │
│ let b = ...; │
│ let c = ...; │
│ └── drop order: c, b, a │
│ │
│ STRUCT FIELDS (declaration order): │
│ struct Pair { │
│ first: ..., │
│ second: ..., │
│ } │
│ └── drop order: first, second │
│ │
│ NESTED SCOPES: │
│ { │
│ let outer = ...; │
│ { │
│ let inner = ...; │
│ } // inner drops first │
│ } // outer drops second │
└──────────────────────────────────────────────────────────────┘
Drop Trait
┌──────────────────────────────────────────────────────────────┐
│ impl Drop for FileHandle { │
│ fn drop(&mut self) { │
│ println!("Closing {}", self.name); │
│ } │
│ } │
│ │
│ { │
│ let f = FileHandle { name: "data.txt".into() }; │
│ // ... │
│ } ← drop is called here │
│ │
│ Drop runs automatically. It cannot be called directly. │
│ Types with Drop cannot be Copy. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Move | Copies stack representation, invalidates source |
| Move cost | Constant (pointer + length + capacity) |
| Heap data | Stays in place |
| Partial move | One field moves, others remain valid |
| Move out of vector | remove, pop, swap_remove |
| Index access | Borrows, does not move |
| Drop | Frees value when owner goes out of scope |
| Drop order (locals) | Reverse of declaration |
| Drop order (struct) | Declaration order |
Drop trait | Customizes cleanup |
std::mem::drop | Drops a value early |
Copy and Drop | Mutually exclusive |
Key takeaways:
- A move copies the stack representation and invalidates the source. The heap data stays in place with a new owner. Moves are constant-time regardless of heap size.
- Partial moves leave unmoved fields valid. Moving one field of a struct invalidates that field but not the others. The struct cannot be used as a whole, but individual valid fields remain accessible.
- Moving out of a collection requires an explicit method.
removeandpopmove elements out. Indexing borrows instead of moving, because the collection must remain in a consistent state. - Drop order is reverse of declaration for locals. Later values drop first. For struct fields, the order is declaration order. The difference exists because locals may depend on earlier locals, while struct fields are dropped after the struct’s own destructor.
- The
Droptrait customizes cleanup. Implementingdroplets a type release resources when it goes out of scope. This is the mechanism behind file handles, locks, and heap deallocation. - A type cannot be both
CopyandDrop. Copying a value would duplicate the resource, and dropping both copies would free it twice. The compiler rejects the combination. std::mem::dropdrops a value early. It takes the value by value and lets it go out of scope, triggering the destructor. It is used to release locks and reduce peak memory.- Drop runs during panic unwinding. A value that goes out of scope because of a panic is still dropped, so resources are released even on failure.
Remember: Move semantics and drop behavior are the two halves of ownership. A move transfers the responsibility for freeing a value; a drop fulfills that responsibility. Together they ensure that every value has exactly one owner, that the owner frees the value when it goes out of scope, and that the free happens exactly once. The compiler tracks all of this statically, inserting drops at the right places and rejecting code that uses a moved value. Understanding what a move actually does — copying the stack part, leaving the heap in place — explains why moves are cheap and why the source becomes invalid. Understanding drop order explains why values are released in a predictable sequence. Understanding the Drop trait explains how resources other than memory are managed. These mechanics are the foundation for writing Rust code that manages memory and resources correctly without a garbage collector.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!