Drop guards and custom cleanup
Drop is the trait with one method, and the compiler calls that method when a value goes out of scope: at the end of a block, on an early return, on a panic that unwinds, or when the value is moved into a call that ends. The method is the place for a lock, a file or a counter, and it runs without a call in the source.
This lesson writes a type whose destructor prints, shows the order the drops run in, uses a guard that closes on an early return, and removes a temporary file from a destructor. The last section is the compiler error for calling the destructor by hand.
A type with a destructor
The trait is small: one method that takes &mut self and returns nothing. A type that implements it cannot be Copy, because a value that is duplicated would be released twice, and the compiler refuses that combination.
The program creates three values with the same destructor. The print at the end of main runs before any destructor, which shows that the drops happen on the way out of the scope.
struct Label(&'static str);
impl Drop for Label {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let first = Label("first");
let second = Label("second");
println!("end of main");
}
rustc --edition 2024 label.rs
./label
end of main
drop second
drop first
The order is the reverse of the declarations, which is the rule C++ also uses for locals. The lesson on Drop, scope and RAII introduces the trait with a value whose release is observable.
The order of the drops
Locals drop in reverse order of declaration, and the fields of a struct drop in declaration order after the struct's own destructor. The two rules are enough to predict a log, and a struct field that must be released after another is placed accordingly.
The program builds a struct of two labels and one local label. The print reads all three, so nothing is unused, and the drops that follow show the two orders side by side.
struct Label(&'static str);
impl Drop for Label {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
struct Pair {
first: Label,
second: Label,
}
fn main() {
let pair = Pair {
first: Label("first"),
second: Label("second"),
};
let local = Label("local");
println!("{} {} {}", pair.first.0, pair.second.0, local.0);
}
rustc --edition 2024 order.rs
./order
first second local
drop local
drop first
drop second
The local goes first because it was declared last, then the struct, whose fields go in the order they are written. A struct with its own Drop runs that method before the fields, so a handle that borrows a field can still use it there. The struct lesson covers field initialisation.
A guard and an early return
A guard is a value that exists only to run a destructor. Binding it to a name at the top of a function makes the cleanup run on every exit path: the normal end, the early return and the unwinding panic.
The program calls the same function twice, once with an early return and once without. The guard closes in both cases, before the caller prints the result.
struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) {
println!("closing {}", self.0);
}
}
fn work(fail: bool) -> i32 {
let _guard = Guard("handle");
if fail {
return -1;
}
42
}
fn main() {
println!("{}", work(true));
println!("{}", work(false));
}
rustc --edition 2024 guard.rs
./guard
closing handle
-1
closing handle
42
The underscore in _guard keeps the value alive for the whole function; a bare _ would drop it at once. That difference is the one trap of the pattern, and it is the reason the binding is named.
A guard that removes a file
The same shape closes a real resource. The guard below owns a path, creates the file when it is built and deletes it in the destructor, so no code path can leave the file behind.
The program creates the guard and checks that the file exists while the guard is alive. The removal line is printed by the destructor at the end of main, and the check afterwards finds nothing.
use std::fs;
use std::path::PathBuf;
struct TempPath(PathBuf);
impl TempPath {
fn new(path: &str) -> Self {
fs::write(path, "temporary").unwrap();
TempPath(PathBuf::from(path))
}
}
impl Drop for TempPath {
fn drop(&mut self) {
fs::remove_file(&self.0).unwrap();
println!("removed {}", self.0.display());
}
}
fn main() {
let temp = TempPath::new("/tmp/drop_demo.txt");
println!("exists {}", temp.0.exists());
}
rustc --edition 2024 temp.rs
./temp
ls /tmp/drop_demo.txt
exists true
removed /tmp/drop_demo.txt
ls: /tmp/drop_demo.txt: No such file or directory
A destructor cannot report an error, so the unwrap here is a decision: a failed removal panics inside the drop, and a panic in a destructor during another panic aborts the process. A library guard usually ignores the error or records it, which is why File flushes on drop and leaves the error to an explicit call. The lesson on text files shows the explicit form.
A panic also runs the destructors, as long as the build unwinds: the guard closes the handle on the way out of a failed assertion exactly as it does on a return. That is why a guard is preferable to a close call at the end of a function, which a panic would skip. Two things break the promise: std::mem::forget, which takes the value and never drops it, and a process that aborts before the unwind.
A destructor called by hand
There is no .drop() method to call: the destructor runs at the end of the scope, and a second run would release the value twice. The compiler reports E0040 and suggests the free function that takes the value by move.
The program calls the method on a guard. The suggestion in the error is the fix, and it is a call to drop with the value as an argument.
struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) {
println!("closing {}", self.0);
}
}
fn main() {
let guard = Guard("handle");
guard.drop();
}
rustc --edition 2024 call_err.rs
error[E0040]: explicit use of destructor method
--> call_err.rs:11:11
|
11 | guard.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
11 - guard.drop();
11 + drop(guard);
|
The free function moves the value into itself and lets the scope end there, which is why it can run the destructor exactly once. The lesson on uninitialised memory uses the same call to end a value early, and ManuallyDrop for the case where it must not run at all.
Exercises
- Write a guard that prints its name and build a struct of three of them; predict the log before you run it.
- Add a Drop to a struct that owns a Vec and print the length from the destructor.
- Make a guard restore a value in its destructor after a function changes it.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
When does the compiler call
Drop::drop?Answer
When the value leaves its scope
The call is inserted at every point where the value goes out of scope.
-
Why can a type with
Dropnot beCopy?Answer
A copy would be released twice
Two values would own the same resource and both would run the destructor.
-
In which order do the locals of a scope drop?
Answer
Reverse order of declaration
The last value created is the first released, like a stack.
-
What does a guard bound at the top of a function guarantee?
Answer
The cleanup runs on every exit path
The value lives until the function returns, whatever the path.
-
What does this program print?
struct Label(&'static str); impl Drop for Label { fn drop(&mut self) { println!("{}", self.0); } } fn main() { let first = Label("a"); let second = Label("b"); }Answer
b / a
The second local is released first, and each destructor prints one letter.
-
What does this program print?
struct Pair { left: Box<i32>, right: Box<i32>, } impl Drop for Pair { fn drop(&mut self) { println!("pair {} {}", self.left, self.right); } } fn main() { let pair = Pair { left: Box::new(1), right: Box::new(2) }; }Answer
pair 1 2
The destructor of the struct reads both fields before they are dropped.
-
What does this program print?
struct Guard; impl Drop for Guard { fn drop(&mut self) { println!("close"); } } fn run() -> i32 { let _guard = Guard; return 7; } fn main() { println!("{}", run()); }Answer
close / 7
The guard drops when run returns, before main prints the result.
-
What does this program print?
struct Guard; impl Drop for Guard { fn drop(&mut self) { println!("close"); } } fn main() { let _ = Guard; println!("after"); }Answer
close / after
A value bound to the underscore pattern is dropped at once, before the next statement.
-
What is the first line of the compiler output for this program?
struct Guard; impl Drop for Guard { fn drop(&mut self) { println!("close"); } } fn main() { let guard = Guard; guard.drop(); }Answer
error[E0040]: explicit use of destructor method
The destructor runs at the end of the scope, and the free function is the way to end a value early.
-
What is the first line of the compiler output for this program?
#[derive(Clone, Copy)] struct Guard; impl Drop for Guard { fn drop(&mut self) {} } fn main() { let guard = Guard; let copy = guard; println!("{}", std::mem::size_of_val(©)); }Answer
error[E0184]: the trait
Copycannot be implemented for this type; the type has a destructorA type with a destructor cannot also be copied, because the destructor would run twice.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev