How does the second mutable borrow break the lifetime bound in this example

pub struct A();

fn run_both<'c>(a: &'c mut A, b: impl Fn(&'c mut A))
{
    b(a);
    b(a);
}

fn main() {
}

above code won't compile, the error message complains that

--> src/main.rs:7:7
  |
4 | fn run_both<'c>(a: &'c mut A, b: impl Fn(&'c mut A))
  |             -- lifetime `'c` defined here
5 | {
6 |     b(a);
  |     ----
  |     | |
  |     | first mutable borrow occurs here
  |     argument requires that `*a` is borrowed for `'c`
7 |     b(a);
  |       ^ second mutable borrow occurs here

but the code compiles successfully if there is only one b(a) in the run_both function.

pub struct A();

fn run_both<'c>(a: &'c mut A, b: impl Fn(&'c mut A))
{
    b(a);
}

fn main() {
}

so my question is what is the meaning of the lifetime annotation 'c, doesn't that mean the object which is referenced by function argument a is valid during the lifespan 'c? so why the second invocation b(a) affect the lifespan of the referent?

Here's an exploitation that demonstrates why a second call to b(a) could be bad

use std::cell::RefCell;

#[derive(Debug)]
pub struct A(i32);

fn run_both<'c>(a: &'c mut A, b: impl Fn(&'c mut A)) {
    b(a);
    //b(a);
}

fn main() {
    let x: RefCell<[Option<&mut A>; 2]> = RefCell::new([None, None]);
    let i = RefCell::new(0);
    let mut val = A(42);
    run_both(&mut val, |r| {
        let mut i = i.borrow_mut();
        x.borrow_mut()[*i] = Some(r);
        *i += 1;
    });
    let references = x.into_inner();
    dbg!(&references);
}

If there were two b(a) calls in run_both (and that didn't result in a compilation error) then in main the references vector would at the end contain two (active) copies of the same mutable reference, which is something you must not be able to obtain in Rust, as you probably know.


The typical fix is to change the function signature. In this case, lifetime elision does actually do the right thing

fn run_both(a: &mut A, b: impl Fn(&mut A))
{
    b(a);
    b(a);
}

Now run_both compiles fine with two calls, but the exploit code above will (successfully) be prevented from compiling.

error[E0521]: borrowed data escapes outside of closure
  --> src/main.rs:18:9
   |
13 |     let x: RefCell<[Option<&mut A>; 2]> = RefCell::new([None, None]);
   |         - `x` declared here, outside of the closure body
...
16 |     run_both(&mut val, |r| {
   |                         - `r` is a reference that is only valid in the closure body
17 |         let mut i = i.borrow_mut();
18 |         x.borrow_mut()[*i] = Some(r);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `r` escapes the closure body here

The meaning of the elided lifetimes involve so-called hither-ranked trait bounds.

fn run_both(a: &mut A, b: impl Fn(&mut A))

is essentially short for something like

fn run_both<'c>(a: &'c mut A, b: impl for<'d> Fn(&'d mut A))

This allows the closure b to be called with a mutable reference of shorter lifetime than 'c. In particular the two calls to b(a) must happen with references of different lifetime since otherwise the references passed to the two calls would be (rightfully, as demonstrated above) considered to be overlapping and thus conflicting aliasing mutable access to the same thing. And the higher-ranked bound for Fn allows the callback to be generic over the lifetime 'd and thus allows those multiple calls with different non-overlapping reference lifetimes.

4 Likes

Another puzzle piece for a better understanding here is to be aware of implicit re-borrowing. One could start by asking the question "why don't I get some sort of use of moved value `a` on the second b(a) call?". After all, mutably references can not be implicitly copied.

The reason why is the the calls to b(a) are implicitly re-written into something like b(&mut *a). This is why your compilation error doesn't tell you that a was moved and used again, but instead talks about mutable borrows of “*a". The code, effectively re-written into

b(&mut *a);
b(&mut *a);

indeed creates two mutable borrows of *a, and the type signature of b requires a &'c mut A for both of these.

The concrete error message then does not speak about the problem that those two borrows would be overlapping because they require the same lifetime, but instead argues that a borrow of lifetime 'c (crested in the first call to b) must last longer than the whole run_both function and that's in turn the reason why the second borrow or *a for the second b call would overlap. It's a different reasoning, incorporating some details of what lifetime parameters exactly mean that we didn't discuss, i. e. that they always describe lifetimes longer than the entire function call, but that's not all that relevant for understanding the problem, it just helps understanding the concrete error message ^^

error[E0499]: cannot borrow `*a` as mutable more than once at a time
 --> src/main.rs:6:7
  |
3 | fn run_both<'c>(a: &'c mut A, b: impl Fn(&'c mut A))
  |             -- lifetime `'c` defined here
4 | {
5 |     b(a);
  |     ----
  |     | |
  |     | first mutable borrow occurs here
  |     argument requires that `*a` is borrowed for `'c`
6 |     b(a);
  |       ^ second mutable borrow occurs here
3 Likes

thanks, this is a really good example. can i consider higher-ranked bound as a kind of constraint which tell the compiler that the closure b will not store the mutable reference in outside world (which is the context of a closure, let's say a closure = function + context). because i fail to come up with a function that can break the correctness of the logic if the lifetime is not 'strict.

another maybe dumb question, does following two statements equal?

impl for<'d> Fn(&'d mut A)
Fn(&_ mut A)

For some basic information on lifetime elision, the relevant chapters of the Rustomomicon are a good read. E. g. here's the one explaining the particular rules for Fn traits (but feel free to also look at previous pages introducing lifetime elision, for better context): Higher-Rank Trait Bounds - The Rustonomicon

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.