Moving Values and Destruction
In Rust, destructors track initialized values, not lexical variables. When ownership of a value is moved from one variable to another (or into a function, return value, or struct field), the source variable becomes logically uninitialized and the compiler skips its destructor at the end of the scope, ensuring the underlying resource is dropped exactly once by its final owner.
Compile-Time Initialization vs. Runtime Drop Flags
- Static tracking (zero runtime cost): When a variable is unconditionally moved before the end of its scope, the compiler statically knows it is uninitialized and emits no destructor call for the original binding.
- Dynamic drop flags (conditional moves): When a variable is moved inside only one branch of an
if/elseormatchexpression, the compiler inserts a hidden 1-byte boolean drop flag on the stack frame. At the end of the scope, the generated drop glue checks the flag and runsDrop::droponly if the variable was not moved.
Conditional Move Drop Flag on Stack:
let resource = Resource::new(); // drop_flag = true
if condition {
consume(resource); // resource moved -> drop_flag = false
}
// Scope exit: if drop_flag { Drop::drop(&mut resource); }
struct Ticket(u32);
impl Drop for Ticket {
fn drop(&mut self) {
println!("Ticket({}) destroyed", self.0);
}
}
fn redeem(t: Ticket) {
println!("redeeming Ticket({}) inside redeem()", t.0);
} // `t` is dropped here when `redeem` returns.
fn run_demo(should_redeem: bool) {
let ticket = Ticket(42);
if should_redeem {
redeem(ticket);
println!("back in run_demo after redeeming");
} else {
println!("skipping redemption in run_demo");
}
println!("exiting run_demo");
}
fn main() {
println!("=== Case 1: Moved into redeem() ===");
run_demo(true);
println!("=== Case 2: Not moved (dropped at end of run_demo) ===");
run_demo(false);
}
=== Case 1: Moved into redeem() ===
redeeming Ticket(42) inside redeem()
Ticket(42) destroyed
back in run_demo after redeeming
exiting run_demo
=== Case 2: Not moved (dropped at end of run_demo) ===
skipping redemption in run_demo
exiting run_demo
Ticket(42) destroyed
Partial Moves and Field-Granular Destruction
When a struct does not implement Drop directly, moving one of its fields out transfers ownership of that single field while leaving the remaining fields owned by the original struct binding. At the end of the block, the compiler drops only the remaining unmoved fields:
struct Part(&'static str);
impl Drop for Part {
fn drop(&mut self) {
println!("dropping Part({:?})", self.0);
}
}
struct Kit {
hammer: Part,
wrench: Part,
}
fn main() {
let kit = Kit {
hammer: Part("hammer"),
wrench: Part("wrench"),
};
// Move `kit.hammer` into an inner scope that ends early:
{
let moved_hammer = kit.hammer;
println!("using {} in inner scope", moved_hammer.0);
} // `moved_hammer` is dropped here.
println!("still using {} in main", kit.wrench.0);
} // Only `kit.wrench` is dropped here!
using hammer in inner scope
dropping Part("hammer")
still using wrench in main
dropping Part("wrench")
Re-initializing a Moved Binding
If a mut binding is moved out of (either completely or partially) and subsequently assigned a new value before the scope ends, its drop flag is set back to initialized and the new value is dropped normally at scope exit.