Why this matters
Ownership promised that memory is freed automatically "when the owner goes out of scope". This lesson looks at that moment closely. It is called drop, and it is not only about memory: the same mechanism closes files, releases locks, flushes buffers and shuts down network connections, exactly when the owning variable's scope ends, every time, even on early returns. This pattern, tying a resource's lifetime to a variable's scope, is called RAII and is one of the most useful ideas in systems programming.
Scope
A variable's scope runs from its declaration to the end of the block { } that contains it:
fn main() {
let outer = 1; // outer valid from here
{
let inner = 2; // inner valid from here
println!("{outer} {inner}");
} // inner's scope ends
println!("{outer}");
} // outer's scope ends
For a String, the end of scope is where the compiler inserts the code that frees the heap buffer. To see it happen, we need a type that announces its own drop.
Watching drops happen
The following type prints a line when it is dropped. It uses two things from later modules: a struct (module 7) and an implementation of the Drop trait (module 13). Read it as a recipe: "when a Noisy is dropped, run this function".
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let _a = Noisy("a");
{
let _b = Noisy("b");
let _c = Noisy("c");
println!("inner block ends");
}
let _d = Noisy("d");
println!("main ends");
}
inner block ends
drop c
drop b
main ends
drop d
drop a
struct Noisy(&'static str);defines a type holding one string literal (&'static stris the type of string literals; the'staticpart is explained in module 10).impl Drop for Noisy { fn drop(&mut self) { ... } }provides the cleanup code. You never calldropyourself; the compiler calls it.- The variable names start with
_only to silence "unused variable" warnings.
Two rules are visible in the output:
- A value is dropped at the end of its owner's scope.
- Within one scope, values are dropped in reverse order of declaration:
cbeforeb,dbeforea. Later values may depend on earlier ones (a buffered writer created on top of a file), so they must be cleaned up first.
String's drop frees its heap buffer, in exactly the same places. Nothing is printed, but it is the same mechanism.
Moves change who drops
A move transfers the responsibility to drop along with ownership:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn consume(n: Noisy) {
println!("consume got {}", n.0);
}
fn main() {
let x = Noisy("x");
let y = Noisy("y");
consume(x);
println!("back in main");
let z = y;
println!("main ends, z holds {}", z.0);
}
consume got x
drop x
back in main
main ends, z holds y
drop y
x was moved into consume's parameter, so it is dropped when consume returns, before "back in main". y was moved into z; it is dropped once, at the end of main, as z. The compiler tracks moved-from variables and does not drop them. This is how ownership guarantees exactly one drop per value: the drop follows the owner.
Dropping early
Sometimes you want to release a resource before the scope ends: close a file so another process can use it, release a lock, free a large buffer before a long computation. Call drop(value):
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let big = Noisy("big buffer");
println!("using it");
drop(big);
println!("long computation without it");
}
using it
drop big buffer
long computation without it
drop (from the standard prelude, really std::mem::drop) is an ordinary function with an empty body: it takes ownership of its argument, and the argument goes out of scope when the function returns. There is no magic; it is the same "moved into a function" rule as consume above. After drop(big), using big is E0382, so you cannot accidentally touch a released resource.
An inner block { } achieves the same and can be clearer when several values should go together.
Reassignment drops the old value
Assigning a new value to a variable that still holds one drops the old value:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let mut slot = Noisy("old");
println!("assigning");
slot = Noisy("new");
println!("assigned");
}
assigning
drop old
assigned
drop new
The new value is created first, then the old one is dropped, then the variable holds the new value. For a String variable in a loop (line = next_line();), each reassignment frees the previous buffer. No leaks accumulate.
Drop runs on every exit path
Because drops are tied to scopes, they run however the scope is left: falling off the end, an early return, break out of a loop, even a panic (the stack is "unwound" and everything is dropped on the way out):
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn check(n: i32) -> bool {
let _guard = Noisy("guard");
if n < 0 {
println!("negative, returning early");
return false;
}
println!("all good");
true
}
fn main() {
check(-1);
check(5);
}
negative, returning early
drop guard
all good
drop guard
In C, every early return is a chance to forget a free or fclose. With drop, cleanup cannot be skipped. That is RAII: acquire the resource when the value is created, release it when the value is dropped, and let scopes do the bookkeeping.
Common mistakes
Calling drop as a method. big.drop() is forbidden:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let big = Noisy("x");
big.drop();
}
error[E0040]: explicit use of destructor method
--> main.rs:11:9
|
11 | big.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
11 | drop(big);
| +++++ ~
E0040: calling the destructor directly would let the value be dropped again at the end of scope. Use drop(big), which takes ownership so the double drop cannot happen.
Binding to _. let _ = Noisy("x"); drops the value immediately, because _ is not a variable and nothing owns the value. let _x = ... keeps it alive until the end of the scope. This matters for guards such as locks.
Expecting drop order to follow last use. A value lives until the end of its scope even if it is last used much earlier (the compiler does not drop "as soon as possible"). Use an inner block or drop to end it sooner.
Leaks are still possible. Ownership prevents double frees and use-after-free. It does not make leaking impossible: std::mem::forget and reference cycles (module 13) can leak. In everyday code they are rare.
Recap
- A value is dropped when its owner's scope ends; within a scope, in reverse declaration order.
- Moving a value moves the duty to drop it; moved-from variables are not dropped.
drop(x)ends a value's life early by moving it into a function; inner blocks do the same.- Reassignment drops the old value; drops run on every exit path, including early returns and panics.
- RAII: resources (memory, files, locks) are released by drop, so cleanup cannot be forgotten.
Going further: add a Noisy inside a loop body and watch it drop on each pass. Then put one in a Vec, push three, and see in which order the vector drops its elements.