How does crossbeam's scope function pass lifetime check?

(This question is not about multi-threading, but about lifetime check)

As we all know, std::thread::spawn can't simply capture variables, and crossbeam::scope can. To simplify, here is the code about lifetime check:

struct Foo;

impl Foo {
    fn foo(&mut self) {}
}

struct Bar<'a> {
    phantom: PhantomData<&'a ()>
}

impl<'a> Bar<'a> {
    fn bar<F: FnMut() + 'a>(&mut self, f: F) {}
}

fn scope<'a, F: FnOnce(&mut Bar<'a>)>(f: F) {}

fn something_cannot_pass_compile<'a>(mut bar: Bar<'a>) {
    let mut foo = Foo;
    bar.bar(|| { foo.foo() });
}

fn something_can_pass_compile() {
    let mut foo = Foo;
    scope(|bar| {
        bar.bar(|| { foo.foo() });
    })
}

Why does something_can_pass_compile can pass the lifetime check while something_cannot_pass_compile not?

That's because you specify in the impl for Bar:

  • The lifetime for Bar is 'a.
  • In something_cannot_pass_compile, foo is a local variable.
  • Therefore, a reference to it cannot outlive the scope of the function.
  • But the bar function specifies that F: FnMut() + 'a. which means that the closure must live as 'a. But the f being passed in lives only as long as the scope of something_cannot_pass_compile. Hence this lifetime cannot be constrained.

The fix is to do (Playground):

impl Bar<'_> {
    fn bar<F: FnMut()>(&mut self, f: F) {}
}
1 Like

Let's look at it from the perspective of the function writers, where the rest of the code is just some API they've been given. It will be more clear if we give better, non-overlapping names to the lifetimes.

struct Bar<'bar> {
    phantom: PhantomData<&'bar ()>
}

// Note that everything called `'bar` here is the _same_ lifetime
impl<'bar> Bar<'bar> {
    fn bar<F: FnMut() + 'bar>(&mut self, f: F) {}
}

fn scope<'scope, F: FnOnce(&mut Bar<'scope>)>(f: F) {}

fn something_cannot_pass_compile<'bar>(mut bar: Bar<'bar>) { /* ... */ }

In something_can_pass_compile, the writer of the function gets to choose the lifetime used. scope is declared in a way that says it can handle any lifetime 'scope that the caller chooses. The implication is that they will create a Bar with the lifetime the caller chooses, then call the closure with that Bar. As written, the author implicitly chooses some lifetime that cannot exceed the local lifetime, due to the use of the local variable foo. If you change the call to explicitly use scope::<'static>, it will fail.

In something_cannot_pass_compile, the writer of the function does not choose the lifetime 'bar. 'bar is a type parameter of the function, and by writing it as such, they have declared they can handle any lifetime 'bar handed to them by the caller, in the form of a Bar<'bar>. But this is not true as written -- the use of Bar<'bar>::bar requires a FnMut() + 'bar, yet as before foo cannot outlive the local lifetime. That is,

  • scope can handle any 'scope, but Bar<'bar>::bar can only handle a specific 'bar
    • 'bar is not a type parameter on the method bar
  • can_pass gets to choose the lifetime, but cannot_pass is handled a pre-determined lifetime
    • If I hand you a Bar<'static>, it must fail just like scope::<'static> must fail. Since you said you could handle any lifetime, the fact that you can't handle 'static is an error.

With @RedDocMD's patch, Bar<'bar>::bar can handle any lifetime, similar to scope. The closure is no longer limited to 'bar. cannot_pass is still handed a pre-determined lifetime in the form of Bar<'bar>, but it doesn't matter when calling Bar<'bar>::bar locally any more -- they can call Bar<'bar>::bar with a closure that has any lifetime at all.

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.