Ownership · about 40 min

Scope and Drop: Deterministic Cleanup

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

Two rules are visible in the output:

  1. A value is dropped at the end of its owner's scope.
  2. Within one scope, values are dropped in reverse order of declaration: c before b, d before a. 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

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.

Exercises

Exercise 1 · quiz

Predict the order

Noisy prints drop NAME when it is dropped. What does this print?

fn main() {
    let a = Noisy("a");
    {
        let b = Noisy("b");
        let c = Noisy("c");
        println!("inner end");
    }
    let d = Noisy("d");
    println!("main end");
}
Answer
  • inner end, drop c, drop b, main end, drop d, drop a

Values are dropped when their scope ends, in reverse order of declaration. The inner block ends first, dropping c then b. At the end of main, d (declared last) goes before a. Reverse order matters because later values may depend on earlier ones, like a writer created from a file: the writer must be finished before the file is closed.

Exercise 2 · write code

Make the output match

The starter defines a Guard type that prints when it is created and dropped. Rewrite main (you may only create Guards, use blocks { }, call drop(...) and println!) so that the output is exactly:

open db
open log
close log
work
close db

File: main.rs

Starting point

struct Guard(&'static str);

impl Guard {
    fn new(name: &'static str) -> Guard {
        println!("open {name}");
        Guard(name)
    }
}

impl Drop for Guard {
    fn drop(&mut self) {
        println!("close {}", self.0);
    }
}

fn main() {
    let db = Guard::new("db");
    let log = Guard::new("log");
    println!("work");
}

Example

expected output

open db
open log
close log
work
close db
Hint 1

log must be closed before work is printed, but db stays open until the end.

Hint 2

Put log in its own inner block, or call drop(log); before printing work.

Reference solution
struct Guard(&'static str);

impl Guard {
    fn new(name: &'static str) -> Guard {
        println!("open {name}");
        Guard(name)
    }
}

impl Drop for Guard {
    fn drop(&mut self) {
        println!("close {}", self.0);
    }
}

fn main() {
    let _db = Guard::new("db");
    {
        let _log = Guard::new("log");
    }
    println!("work");
}

Exercise 3 · write code

What reassignment drops

Using the same Guard type (it is in the starter), write a main that produces exactly:

open first
open second
close first
end
close second

Use a single let mut g = Guard::new("first"); and then assign a new Guard to g.

File: main.rs

Starting point

struct Guard(&'static str);

impl Guard {
    fn new(name: &'static str) -> Guard {
        println!("open {name}");
        Guard(name)
    }
}

impl Drop for Guard {
    fn drop(&mut self) {
        println!("close {}", self.0);
    }
}

fn main() {
    let g = Guard::new("first");
    println!("end");
}

Example

expected output

open first
open second
close first
end
close second
Hint 1

g = Guard::new("second"); first creates the new Guard, then drops the old value of g.

Reference solution
struct Guard(&'static str);

impl Guard {
    fn new(name: &'static str) -> Guard {
        println!("open {name}");
        Guard(name)
    }
}

impl Drop for Guard {
    fn drop(&mut self) {
        println!("close {}", self.0);
    }
}

fn main() {
    let mut g = Guard::new("first");
    g = Guard::new("second");
    println!("end");
}

Exercise 4 · quiz

Who drops a moved value?

fn consume(n: Noisy) { println!("consuming"); } is called as consume(x); in main, followed by println!("back in main");. When is x's value dropped?

Answer
  • At the end of consume, before "back in main" is printed, because consume's parameter owns it

Moving transfers the responsibility to drop. The parameter n owns the value, and n goes out of scope when consume returns, so the drop happens there. main's x is moved-from; the compiler knows that and does not drop it again. Exactly one drop, always, at a place determined at compile time.