Rust is shrinking lifetimes too much when passed to closures

This is very inconvenient to have that code not compiling: playground

It makes a lot of problems during embedded development, requiring to do stuff like Opaque, that makes code very unpleasant to read and write.

#![allow(dead_code)]
#![allow(clippy::all)]
#![allow(unused_must_use)]

fn main() {}

struct Opaque(&'static mut ());
fn use_static(_: &'static mut ()) { }
fn use_static2(op: Opaque) { use_static(op.0) }

fn compiles(foo: &'static mut (), bar: &'static mut ()) {
    let foo = Opaque(foo);
    let bar = Opaque(bar);
    || use_static2(foo) ;
    move || use_static2(bar);
}

// #[cfg(skip)] // remove that line and it will not compile
fn error(foo: &'static mut (), bar: &'static mut ()) {
    || use_static(foo);
    move || use_static(bar);
}
   Compiling playground v0.0.1 (/playground)
error: lifetime may not live long enough
  --> src/main.rs:20:8
   |
20 |     || use_static(foo);
   |     -- ^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'static`
   |     |
   |     lifetime `'1` represents this closure's body
   |
   = note: closure implements `FnMut`, so references to captured variables can't escape the closure

error: lifetime may not live long enough
  --> src/main.rs:21:13
   |
21 |     move || use_static(bar);
   |     ------- ^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'static`
   |     |
   |     lifetime `'1` represents this closure's body
   |
   = note: closure implements `FnMut`, so references to captured variables can't escape the closure

error: could not compile `playground` (bin "playground") due to 2 previous errors

This compiles too:

fn use_static3(_: (&'static mut (),)) { }

fn tuples(foo: &'static mut (), bar: &'static mut ()) {
    let foo = (foo,);
    let bar = (bar,);
    || use_static3(foo);
    move || use_static3(bar);
}
fn also_compiles2(foo: &'static mut (), bar: &'static mut ()) {
    let (foo, bar) = ((foo,),(bar,));
    fn transform(foo: (&'static mut (),)) -> &'static mut () {
        foo.0
    }
    || use_static(transform(foo));
    move || use_static(transform(bar));
}

I'm guessing the issue is that if the reference is only used in ways that could use reborrowing, the closure won't capture it by value. Regardless, this compiles:

fn compiles(foo: &'static mut (), bar: &'static mut ()) {
    || { 
        let foo = foo;
        use_static(foo);
    };
    move || { 
        let bar = bar;
        use_static(bar);
    };
}

edit: Specifically, this is best explained by this section in the Reference https://doc.rust-lang.org/reference/types/closure.html#call-traits-and-coercions

  • A closure which does not move out of any captured variables implements FnMut, indicating that it can be called by mutable reference.

Note: move closures may still implement Fn or FnMut, even though they capture variables by move. This is because the traits implemented by a closure type are determined by what the closure does with captured values, not how it captures them.

As was evident (though not obvious that it was critical) in the error message, the error is due to the closure "looking like" it should implement FnMut, when that implementation is not actually possible due to lifetimes.

2 Likes

Yes, we need some better way to override closure inferences generally.

Another workaround for the playground is to funnel the closure through an identity function expecting FnOnce, as that will influence the compiler to not implement FnMut.

fn funnel<F: FnOnce()>(f: F) -> F { f }

fn error(foo: &'static mut (), bar: &'static mut ()) {
    funnel(|| use_static(foo));
    funnel(move || use_static(bar));
}
3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.