Why would `mut` reference interfere with lifetime resolution in move closures?

When I run the following code

struct S;

impl S {
    fn data(&self) -> &[u8] {
        &[]
    }
}

// Works
fn f_1(x: &mut S) -> &[u8] {
    S::data(x)
}

// LIFETIME ERROR!
fn f_2(x: &mut S) -> &[u8] {
    let closure = move || S::data(x);
    closure()
}

I get the error below (link to playground):

   Compiling playground v0.0.1 (/playground)
error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements
  --> src/lib.rs:16:27
   |
16 |     let closure = move || S::data(x);
   |                           ^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the lifetime `'_` as defined on the body at 16:19...
  --> src/lib.rs:16:19
   |
16 |     let closure = move || S::data(x);
   |                   ^^^^^^^^^^^^^^^^^^
note: ...so that closure can access `x`
  --> src/lib.rs:16:35
   |
16 |     let closure = move || S::data(x);
   |                                   ^
note: but, the lifetime must be valid for the anonymous lifetime defined on the function body at 15:11...
  --> src/lib.rs:15:11
   |
15 | fn f_2(x: &mut S) -> &[u8] {
   |           ^^^^^^
note: ...so that reference does not outlive borrowed content
  --> src/lib.rs:17:5
   |
17 |     closure()
   |     ^^^^^^^^^

Some questions:

  • Why would f_2 give me the lifetime error below? Like, closure should now be an anonymous struct holding x (i.e., x got moved into closure) and invoking the closure should return a slice with a lifetime of S.
  • Why would the error disappear if I remove mut from &mut S? Which is, even if my explanation before is wrong, why would adding or removing mut change the lifetime reasoning here? There does not seem to be any aliasing issue involved.
  • What can I do to fix this without removing mut or move?

The closure tries to implement Fn and FnMut and fails to do so.

Let’s give the lifetime a name

fn f_2<'a>(x: &'a mut S) -> &'a [u8] {
    let closure = move || S::data(x);
    closure()
}

First of all, S::data(x) implicitly re-borrows the mutable reference immutable, i.e. it’s the same as S::data(&*x). This does not require mutable access to x, which is why the rustc compilers considers implementing Fn for the closure in the first place (as opposed to only implementing FnOnce and FnMut, but not Fn; or only FnOnce).

Capturing &'a mut S, the closure implementing Fn must implement a method fn call<'b>(&'b self) -> &'a [u8]. Inside, via &'b Self it can only get access to &'b &'a mut S which cannot be turned into &'a mut S.

Implementing FnMut would have the same problem, as you cannot turn &'b mut &'a mut S into &'a mut S either. Only FnOnce would work.

If you add something to the closure that rules out anything but FnOnce, it suddenly compiles:

fn f_2(x: &mut S) -> &[u8] {
    struct Foo; // does not implement Copy
    let foo = Foo;
    let closure = move || {
        drop(foo); // can only happen once
        S::data(x)
    };
    closure()
}

Alternatively, making sure to only capture a &'a S instead of &'a mut S in the first place helps, too. This is because &'b &'a S can be turned into &'a S (since &S is Copy). This is how it can be done:

fn f_2(x: &mut S) -> &[u8] {
    let x = &*x;
    let closure = move || S::data(x);
    closure()
}

Edit: Why does removing move change anything? Why does this work?:

fn f_2(x: &mut S) -> &[u8] {
    let closure = || S::data(x);
    closure()
}

The answer is, the new 2021 edition helps out! With 2018 edition without move, you get an error as-well. However 2021 edition will see the implicit reborrow S::data(&*x) and only borrow *x, not x itself. In-fact *x will be borrowed immutably (since it’s accessed immutably) so that what’s actually stored in the closure is already an immutable re-borrow “&*x” of the mutable reference (so pretty much the same as the explicit let x = &*x; let closure = move || ...; version).

1 Like

Woah thanks a bunch for the detailed instructions! Is that something I'm supposed to know, or is this a compiler issue?

Like you write

The closure tries to implement Fn and FnMut and fails to do so.
...
If you add something to the closure that rules out anything but FnOnce , it suddently compiles

I would have never arrived at that conclusion myself, it seems very hacky introducing Foo. Shouldn't the compiler do that for me?

Edit: In particular when you say " closure tries to implement Fn and FnMut and fails to do so" I'm like "I don't care, I just want to call that method and the compiler should see that all I do is trying to call that method and it should be sound trying to call that method"

Took me a few minutes as-well to figure it out. In my opinion, the compiler should either somehow decide by itself that FnMut and Fn can’t be implemented, or at least give better diagnostics, i.e. an error message proposing possible fixes. I’d consider the current behavior a compiler issue. I’m not sure if any issue is already open on GitHub on this; in case you don’t find any, feel free to open one yourself.

The rules are currently a fairly clearly specified heuristic: Implementation of Fn/FnMut depends on whether captured variables are accessed mutably or immutably. Arguably someone could be confused if the compiler just decides to skip the FnMut and Fn implementation here, but as I said above, it does seem like a reasonable goal to me.

By the way, I just found a different workaround to force FnOnce-only: Prevent the reborrow by moving x into a new variable inside of the closure.

fn f_2(x: &mut S) -> &[u8] {
    let closure = move || {
        let x = x;
        S::data(x)
    };
    closure()
}
1 Like

In my opinion, the compiler should either somehow decide by itself that FnMut and Fn can’t be implemented, or at least give better diagnostics

Yes, that would have been nice.

Thank you so much for explaining all of that!

Another way to do that is with braces: S::data(&*{x})

Okay, I only tried S::data({x}) which still re-borrows. In my opinion, given how easily they’re introduced implicitly, the compiler should really try to eliminate all cases where a re-borrow breaks code that would’ve compiled fine with a move. In particular things like the fact that

fn f_2(x: &mut S) -> &[u8] {
    let closure = move || {
        let x: _ = x;
        S::data(x)
    };
    closure()
}

works, but

fn f_2(x: &mut S) -> &[u8] {
    let closure = move || {
        let x: &mut _ = x;
        S::data(x)
    };
    closure()
}

doesn’t (because let x: &mut _ = introduces a re-borrow) are way too subtle. (I’d personally prefer to try to make let x = x re-borrow as-well, but eliminate all the cases where this could currently lead to code breakage.)

I think if I ever saw this in someone's code I could never guess what this actually does.

It's somewhat dated (e.g. written when Rust still had lexical lifetimes), but here's an article about the identity function in Rust... aka {x}.

2 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.