Move does not seem to be capturing environement by value

I was writing code that basically summed to something like this:

struct Thing<'a> {
    thingy: Option<&'a str>,
}

impl<'a> Thing<'a> {
    fn thingy(&mut self, thingy: &'a str) {
        self.thingy = Some(thingy);
    }
}

fn perform<F>(f: F)
where F: FnOnce() + Send + Sync + 'static
{
    f()
}

fn main() {
    let mut thing = Thing { thingy: None };
    perform(move || {
        let thingy = "hello world".to_string();
        thing.thingy(&thingy);
    });
}

Since I'm using move, I expected thing to be moved inside the closure, but I was surprised that it wasn't, and I got this error instead:

error[E0597]: `thingy` does not live long enough
  --> src/main.rs:22:22
   |
18 |     let mut thing = Thing { thingy: None };
   |         --------- lifetime `'1` appears in the type of `thing`
...
22 |         thing.thingy(&thingy);
   |         -------------^^^^^^^-
   |         |            |
   |         |            borrowed value does not live long enough
   |         argument requires that `thingy` is borrowed for `'1`
23 |     });
   |     - `thingy` dropped here while still borrowed

This can be fixed by manually moving thing inside the closure like so:

fn main() {
    let thing = Thing { thingy: None };
    perform(move || {
        let mut thing = thing;
        let thingy = "hello world".to_string();
        thing.thingy(&thingy);
    });
}

Yet, I am puzzled, isn't that what move is for? Why is rust borrowing thing, despite the closure being move?

Note that move is not even necessary in the later case:

fn main() {
    let thing = Thing { thingy: None };
    perform(|| {
        let mut thing = thing;
        let thingy = "hello world".to_string();
        thing.thingy(&thingy);
    });
}

You probably did move thing. But Thing::thingy is only callable on a Thing with lifetime 'a, and it expects an &'a str argument, so you can't pass it a smaller temporary borrow. Whatever you pass it must outlive the Thing, and thus the closure that owns it. So you can't create that within the closure. Perhaps Thing::thingy should take ownership of its argument.

Using move doesn't erase all lifetime problems, and closures are just simple structs. Read more on my article on closures:

1 Like

Thanks for the article, this is an interesting read.

you say:

// note: a new lifetime parameter will be created for
// user-defined structs that also have lifetime parameters.

So in this case I'd assume the lifetime would be the 'a of Thingy, that is in our case the span of main. What is the new lifetime parameter you are talking about?

So if I understand well, If I manually move thing inside of the closure, then it's lifetime changes and matches that of &thingy, but using move, rust is not able to do the same, and the lifetime of thing is still tied to that of main?

Yep, that's exactly the same, however your second (working) version also brings in concepts of variance and subtyping

In your case here, you have the lifetime 'a in Thingy<'a>. The closure captures this and creates some anonymous type

struct __closure__<'a> {
    thing: Thingy<'a>,
}

fn main() {
    let thing = Thing { thingy: None };
    perform(move || {
        let thingy = "hello world".to_string();
        thing.thingy(&thingy);
    });
}

// this becomes ...

fn main() {
    let thing = Thing { thingy: None };
    perform(__closure__ { thing });
}

Now, because perform has a 'static bound, this enforces that thing: Thingy<'static>. So when you do

let thingy = "hello world".to_string();
thing.thingy(&thingy);

The lifetime of &thingy is clearly not 'static, so Rust complains!

If this is all true, then why does this work?

fn main() {
    let thing = Thing { thingy: None };
    perform(move || {
        let mut thing = thing;
        let thingy = "hello world".to_string();
        thing.thingy(&thingy);
    });
}

Here we get to variance. I'll refer you to the nomicon for this, because that's a huge topic. More than I can do in this post alone. Come back after reading that. I also recommend @jonhoo's video on subtyping and variance: Crust of Rust: Subtyping and Variance - YouTube

Ok, read it? We can see that Thingy<'a> is covariant in 'a. This means it's OK to shorten that lifetime. So what happens is that

fn main() {
    let thing = Thing { thingy: None }; // has type Thingy<'static>
    perform(move || {
        // the new thing has type Thing<'thingy>
        // ('thingy is the lifetime of thingy)
        // and because 'thingy is shorter than 'static, Thingy<'thingy>
        // is a subtype of Thingy<'static>, so this assignment is valid
        let mut thing = thing;
        let thingy = "hello world".to_string();
        thing.thingy(&thingy);
    });
}

So everything here is fine.

2 Likes

thanks a lot for the explanation, very clear :slight_smile: