Variable capture by closure

I got a bit confused about how closures capture variables, and how it relates to specifying explicitly which function trait the closure implements. Specifically:

fn main() {
    let mut aa = "aa".to_string();
    let closure = || {aa=="bb".to_string(); 42};
    let boxed /*: Box<dyn FnOnce() -> i32>*/ = Box::new(closure);
    aa;
}

The above code will compile, however when you umcomment the type annotation, the code does not compile anymore (error message says that borrow of 'aa' occurs in the closure). Why doesn't the compiler complain when there is no annotation, even though from what I understand, all closures implement the FnOnce trait?
Thanks

The closure does indeed implement FnOnce and the trait object cast is valid. Read the error message:

error[E0505]: cannot move out of `aa` because it is borrowed
 --> src/main.rs:5:5
  |
3 |     let closure = || {aa=="bb".to_string(); 42};
  |                   --  -- borrow occurs due to use in closure
  |                   |
  |                   borrow of `aa` occurs here
4 |     let boxed: Box<dyn FnOnce() -> i32> = Box::new(closure);
5 |     aa;
  |     ^^ move out of `aa` occurs here
6 | }
  | - borrow might be used here, when `boxed` is dropped and runs the destructor for type `Box<dyn FnOnce() -> i32>`

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

Most types can be dropped even when the references they contain are dangling. However, if you[1] implement Drop for a type that contains a reference, Rust will require that the reference is valid when Drop::drop is run.

When you don't put the type annotation, the Box contains an unnameable type that is specific to the closure. Rustc errors show this type like Box<{closure@src/main.rs:3:19: 3:21}> and rust-analyzer shows it like Box<impl FnOnce() -> i32>. Rust knows this type doesn't need a valid aa to be dropped, so it's fine with aa being dropped first.

When you do put the type annotation, the assignment coerces this unnameable Box into a Box<dyn FnOnce() -> i32> trait object. Trait objects don't contain type information about their drop behavior, but they do have one lifetime generic. In this case, it stores the lifetime of an &aa, which the closure has created and stored. Conservatively, Rust assumes this lifetime must be valid when the trait object is dropped, so it produces the error when you try to drop aa first.

Here's one that doesn't use closures:

#[derive(Debug)]
struct Ref<'a>(&'a String);

fn main() {
    let aa = "aa".to_string();
    let r = Ref(&aa);
    let _b: Box<dyn std::fmt::Debug + '_> = Box::new(r);
    drop(aa); // error
}

  1. "you" meaning "anything outside of the standard library". Standard library types like Box and Vec have a special #[may_dangle] attribute that lets them run their own destructor even when they contain dangling references. ↩ī¸Ž

3 Likes

Also here's one that has the error without involving trait objects. Hopefully this makes it a little clearer why the trait object needs to work the way it does.

struct Ref<'a>(&'a String);

impl Drop for Ref<'_> {
    fn drop(&mut self) {
        println!("dropping"); // we could look at the contained string here
    }
}

fn main() {
    let aa = "aa".to_string();
    let r = Ref(&aa);
    drop(aa);
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.