How to determine the lifetime requirement of returned closure

Let's consider this piece of code

use std::marker::PhantomData;

struct Ctx<'c> {
    val: PhantomData<&'c i32>,
}

impl Ctx<'_> {
    fn insert(&mut self, _val: usize) {
        todo!()
    }
}

struct Foo;
impl Foo {
    fn get_value(&mut self) -> usize {
        todo!()
    }
    fn func<'a, 'r: 'a>(&'a mut self, ctx: &'a mut Ctx<'r>) -> impl FnMut() -> usize + 'a + 'r {
        move || {
            let id = self.get_value();
            ctx.insert(id);
            id
        }
    }
}

The compiler requires 'a:'r and it passes if we changed to that.
But why we need 'a:'r for this? I think it should be all right that &mut self, &mut Ctx and the closure live shorter than 'r.
In fact such code can be compiled

impl Foo {
    fn func_cl<'a, 'r: 'a>(&'a mut self, ctx: &'a mut Ctx<'r>) -> Closure<'a, 'r> {
        Closure { ctx, foo: self }
    }
}

struct Closure<'a, 'r: 'a> {
    ctx: &'a mut Ctx<'r>,
    foo: &'a mut Foo,
}

But this is rejected

// Open these gates:
// #![feature(unboxed_closures)] 
// #![feature(fn_traits)]
impl Foo{
    fn func_cl<'a, 'r: 'a>(&'a mut self, ctx: &'a mut Ctx<'r>) -> impl FnMut() -> usize + 'a + 'r {
        Closure { ctx, foo: self }
    }
}

struct Closure<'a, 'r: 'a> {
    ctx: &'a mut Ctx<'r>,
    foo: &'a mut Foo,
}

impl FnOnce<()> for Closure<'_, '_> {
    type Output = usize;
    extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
        let id = self.foo.get_value();
        self.ctx.insert(id);
        id
    }
}

impl FnMut<()> for Closure<'_, '_> {
    extern "rust-call" fn call_mut(&mut self, _args: ()) -> usize {
        let id = self.foo.get_value();
        self.ctx.insert(id);
        id
    }
}

I guess something similar to #60670 holds it but cannot clarify the reason inside. (If we change all references here to immutable references all will pass).

First let's note that 'r: 'a is implied, and in combination with 'a: 'r, this means the lifetimes must be the same. And &'a mut Thing<'a> is pretty much never what you want.

In edition 2021, RPIT like -> impl FnMut() -> usize implicitly captures any generic types that are inputs to the function, but does not capture any generic lifetimes that are inputs to the function. This will change in edition 2024, so that the generic lifetimes are also implicitly captured.[1] And you can see here that the example compiles on edition 2024 with no + 'a + 'r.

So what's the difference between the implicit capturing and + 'a + 'r? An outlives bound like X: 'x is a bound that requires X to be valid for at least 'x, and + 'r + 'a is applying such a bound to the opaque return type. But (when the lifetimes are not equal) the return type is not actually valid for 'r, it is only valid for the shorter lifetime 'a -- it can't use the captured values after 'a. So an outlives bound is not what you want. But you can't just let the lifetime relationship go unmentioned -- that would basically be the function doing something not declared by the signature / API contract, like cloning a generic type without a Clone bound.

So, you need a way to mention that the lifetime is related to the opaque type without requiring the bound. The typical way to do this is with the "Captures trick":

// This trait conveys no *functionality*, and every type implements it
// for every lifetime.  It's just a way to convey the relationship of
// a lifetime to the opaque type of an RPIT.
trait Captures<'a> {}
impl<T: ?Sized> Captures<'_> for T {}

// ...

    fn func<'a, 'r: 'a>(&'a mut self, ctx: &'a mut Ctx<'r>) 
       -> impl FnMut() -> usize + Captures<'r> + Captures<'a>
    {

You can read more about it in this RFC.


Your Closure<'a, 'r> mentions both lifetimes without requiring a 'r outlives bound.

Your other rejected code has the same outlives bound issues as the main example.

Once edition 2024 lands, the problem will switch from RPITs implicitly undercapturing to RPITs implicitly overcapturing. There's another (fast moving) RFC in the works to address that, too.


  1. This is already the case for async functions (which are a form of RPIT) and for RPITITs, that is, returning impl Trait from a trait method. ↩ī¸Ž

5 Likes

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.