Drop, scope and RAII

In Rust the other half of ownership is release. When the owner goes out of scope the value is dropped, and for a type that implements Drop that means a method runs. A file is closed, a lock is released, a buffer is freed, and the code that does it is the same code that opened it.

Coming from C++? RAII and Drop runs a guard through an early return in both languages, then copies it in C++ and moves it in Rust, and the difference in the closing lines is the whole idea.

This lesson writes guards that print when they are dropped, because a print is the only way to see a destructor in a short program. The order of those lines is the order the compiler destroys values in, and it is worth knowing before the lessons on smart pointers.

The Drop trait

impl Drop for T gives a type one method, fn drop(&mut self), and the compiler calls it when the value is released. The method may not be called directly, and a type with a destructor may not be Copy: a value that can be duplicated implicitly would be destroyed twice.

The program below creates one value and prints two lines. The first comes from main, the second from the destructor that runs at the closing brace.

struct Noisy; impl Drop for Noisy { fn drop(&mut self) { println!("dropping"); } } fn main() { let _item = Noisy; println!("created"); }

rustc --edition 2024 drop_trait.rs
./drop_trait

created
dropping

The binding is named with a leading underscore because nothing reads it. It still exists, and it is still dropped; the underscore only silences the unused-variable warning from the lesson on warnings.

The order of destruction

Values in one scope are dropped in the reverse of their declaration order. The second binding is created after the first, so it is destroyed first, and the program prints the closing lines in that order. This is not an implementation detail: code that depends on a lock being released before a buffer is freed relies on it.

The struct below carries a name so the output says which value was destroyed. The line from main comes first because it runs before the scope ends.

struct Tag(&'static str); impl Drop for Tag { fn drop(&mut self) { println!("drop {}", self.0); } } fn main() { let _first = Tag("first"); let _second = Tag("second"); println!("end of main"); }

rustc --edition 2024 order.rs
./order

end of main
drop second
drop first

Fields inside a struct are dropped in declaration order, and a tuple in the order of its elements, while local variables go in reverse. The rule is worth writing down once and remembering, because the two directions look inconsistent until the stack is in mind.

An early return still drops

A return leaves the function immediately, and every value that was created in the scope is destroyed on the way out. That is the property that makes RAII work: a guard is released whether the function reaches its last line or leaves in the middle.

The function below is called twice, once with the early return and once without it. Both calls print the guard line, which is the whole proof.

struct Guard; impl Drop for Guard { fn drop(&mut self) { println!("guard released"); } } fn work(stop: bool) { let _guard = Guard; if stop { println!("returning early"); return; } println!("finishing"); } fn main() { work(true); work(false); }

rustc --edition 2024 early_return.rs
./early_return

returning early
guard released
finishing
guard released

A panic unwinds the same way, which is why a guard protects a critical section even when the code inside it panics. The lesson on panics shows the unwinding path and the case where it is turned off.

A move moves the drop

Passing a value to a function moves it, and the destructor then runs where the value now lives: at the end of the callee. The caller does not drop it again, because it no longer owns it. A value that is returned moves out before the local scope ends, so the destructor runs in the caller instead.

The program below passes a tagged value to take. The line from main after the call comes last, which shows that the destruction happened inside the function, between the two prints.

struct Tag(&'static str); impl Drop for Tag { fn drop(&mut self) { println!("drop {}", self.0); } } fn take(tag: Tag) { println!("taken {}", tag.0); } fn main() { let tag = Tag("moved"); take(tag); println!("back in main"); }

rustc --edition 2024 moved.rs
./moved

taken moved
drop moved
back in main

This is where a C++ copy constructor and a Rust move part ways: a copied guard would be destroyed twice, once in the callee and once in the caller. The comparison page runs exactly that mistake in C++.

Dropping a value by hand

drop(value) is a function that takes ownership and returns nothing, so the value is destroyed at that point rather than at the end of the scope. It is the way to release a lock or a file before a long computation that does not need it.

The binding is moved into drop, so using it afterwards is E0382, exactly as in the previous lesson. The error is useful here: it proves that the release really happened at the call and not later.

fn main() { let text = String::from("hello"); drop(text); println!("{text}"); }

rustc --edition 2024 drop_err.rs

error[E0382]: borrow of moved value: `text`
--> drop_err.rs:4:16
|
2 | let text = String::from("hello");
| ---- move occurs because `text` has type `String`, which does not implement the `Copy` trait
3 | drop(text);
| ---- value moved here
4 | println!("{text}");
| ^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | drop(text.clone());
| ++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

The lesson on custom drop builds a wrapper around a handle that closes it in the destructor, which is the shape every resource guard has.

Exercises

  1. Add a third tagged value to the order program and predict the closing lines before you run it.
  2. Make the early-return program drop its guard before the early return, using drop. Where does the guard line move to?
  3. Write a guard that increments a counter in its destructor, and print the counter after a block. What does the program prove about the destructor?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. When does the Drop implementation of a value run?

    Answer

    When the value is released at the end of its owner scope

    The release is placed by the compiler at the end of the owner scope, or earlier for an explicit drop.

  2. In what order are two local bindings in one scope dropped?

    Answer

    In reverse declaration order

    Locals are destroyed like a stack: the last one created is the first one released.

  3. What does drop(value) do?

    Answer

    It takes ownership and releases the value at that point

    drop is an ordinary function that consumes its argument, so the release happens at the call.

  4. What happens to a guard when the function returns early?

    Answer

    It is released on the way out of the function

    Every value created in the scope is destroyed as the function returns, however it returns.

  5. What does this program print? struct Noisy; impl Drop for Noisy { fn drop(&mut self) { println!("dropping"); } } fn main() { let _item = Noisy; println!("created"); }

    Answer

    created / dropping

    The line from main runs first, and the destructor runs at the closing brace.

  6. What does this program print? struct Tag(&'static str); impl Drop for Tag { fn drop(&mut self) { println!("drop {}", self.0); } } fn main() { let _first = Tag("first"); let _second = Tag("second"); println!("end of main"); }

    Answer

    end of main / drop second / drop first

    The bindings are destroyed in reverse order, after the last statement of main.

  7. What does this program print? struct Guard; impl Drop for Guard { fn drop(&mut self) { println!("guard released"); } } fn work(stop: bool) { let _guard = Guard; if stop { println!("returning early"); return; } println!("finishing"); } fn main() { work(true); }

    Answer

    returning early / guard released

    The early return leaves the function, and the guard is released as the scope ends.

  8. What does this program print? struct Tag(&'static str); impl Drop for Tag { fn drop(&mut self) { println!("drop {}", self.0); } } fn take(tag: Tag) { println!("taken {}", tag.0); } fn main() { let tag = Tag("moved"); take(tag); println!("back in main"); }

    Answer

    taken moved / drop moved / back in main

    The value is owned by the parameter, so it is dropped at the end of take, before main continues.

  9. What is the first line of the compiler output for this program? fn main() { let text = String::from("hello"); drop(text); println!("{text}"); }

    Answer

    error[E0382]: borrow of moved value: text

    drop moves the value, and the print afterwards borrows the binding that no longer owns anything.

  10. What does this program print? struct Tag(&'static str); impl Drop for Tag { fn drop(&mut self) { println!("{}", self.0); } } fn main() { let _a = Tag("a"); { let _b = Tag("b"); } println!("end"); }

    Answer

    b / end / a

    The inner block ends first and releases b, then main prints, then a is released at the end of main.

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