Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A value that is no longer borrowed causes a "does not live long enough" error

Tags:

rust

This program cannot be compiled.

struct F<'a>(Box<dyn Fn() + 'a>);

fn main() {
    let mut v = vec![]; // Vec<F>
    let s = String::from("foo");

    let f = F(Box::new(|| println!("{:?}", s)));
    v.push(f);

    drop(v);
}
error[E0597]: `s` does not live long enough
  --> src/main.rs:7:44
   |
7  |     let f = F(Box::new(|| println!("{:?}", s)));
   |                        --                  ^ borrowed value does not live long enough
   |                        |
   |                        value captured here
...
11 | }
   | -
   | |
   | `s` dropped here while still borrowed
   | borrow might be used here, when `v` is dropped and runs the `Drop` code for type `Vec`
   |
   = note: values in a scope are dropped in the opposite order they are defined

When s is dropped(line 11), v is already dropped, so s is not borrowed. But the compiler said that s was still borrowed. why?

like image 855
keke Avatar asked Sep 07 '25 00:09

keke


1 Answers

This is due to the consideration that a panic could happen as a result of any function call, since there is no decoration on functions indicating whether they might panic.

When a panic occurs, the stack unwinds and the drop code for each (initialized) variable is run in the opposite of their declaration order (s is dropped first, and then v) But v has the type Vec<F<'a>> where 'a is the lifetime of s, and F implements Drop, which means that s cannot be dropped before v because the compiler can't guarantee that the drop code for F won't access s.

The compiler cannot tell that there isn't actually a memory safety issue here (if push panics, the vector doesn't reference s through the closure). All it knows is that the type of v must live at least as long as s; whether v actually contains a reference to s is immaterial.

To fix this, just swap the order v and s are declared in, which will guarantee that v is dropped before s.


But why does F implement Drop in the first place?

Note that the problem goes away if you remove the Fn() trait object and push the closure directly (e.g. without dyn). This case is different because the compiler knows that the closure doesn't implement Drop -- the closure didn't move-capture any values that implement Drop. Therefore, the compiler knows that s will not be accessed by v's drop code.

By comparison, trait objects always have a vtable slot for Drop::drop, and so the compiler must pessimistically assume that every trait object could have a Drop implementation. This means that when the Vec and Box are destroyed, the compiler emits code to call the trait object's drop code, and based on the information the compiler has, that can result in an access to s since the F value captures the lifetime of s.

This is one of the pitfalls about type erasure through trait objects: the trait object is opaque to the compiler and it can no longer verify that s won't be used by a Drop implementation of a boxed closure after s is dropped. If an owned trait object captures a lifetime, the compiler has to ensure that the captured lifetime does not end before the trait object is dropped.


The above is actually a somewhat simplified explanation. Rust's drop-checker is a bit more complex than this; it's okay if F auto-implements Drop so long as the drop-checker determines that the lifetime 'a doesn't get used. Because of the trait object, this can't be guaranteed. However, this code can compile with a Box holding a non-dyn closure as the drop-checker determines that the captured lifetime isn't used when dropping the box.

like image 104
cdhowie Avatar answered Sep 10 '25 07:09

cdhowie