Lifetimes question (from Rustonomicon)

In the Rustonomicon, there is an example in the "Limits of Lifetimes" section that demonstrates a case where it seems like it should work but doesn't compile but I haven't completely understood the explanation why

Here is the code example:

struct Foo;

impl Foo {
    fn mutate_and_share(&mut self) -> &Self { &*self }
    fn share(&self) {}
}

fn main() {
    let mut foo = Foo;
    let loan = foo.mutate_and_share();
    foo.share();
}

Here is how the Rustinomicon desugars the above code to explain the lifetime problem:

struct Foo;

impl Foo {
    fn mutate_and_share<'a>(&'a mut self) -> &'a Self { &'a *self }
    fn share<'a>(&'a self) {}
}

fn main() {
    'b: {
        let mut foo: Foo = Foo;
        'c: {
            let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo);
            'd: {
                Foo::share::<'d>(&'d foo);
            }
        }
    }
}

Then it explains the reason it doesn't work as: "The lifetime system is forced to extend the &mut foo to have lifetime 'c , due to the lifetime of loan and mutate_and_share's signature. Then when we try to call share , and it sees we're trying to alias that &'c mut foo and blows up in our face!"

I don't understand why the parameter passed to mutate_and_share gets in the way here. Is it because there is a tmp variable created by the compiler that is getting an unnecessarily long lifetime?

Sort of like this...

        'c: {
            let tmp: &'c mut Foo = &mut foo;
            let loan: &'c Foo = Foo::mutate_and_share::<'c>(tmp);
            'd: {
                Foo::share::<'d>(&'d foo);
            }
        }

My theory: in order to call mutate_and_share() the compiler creates a mutable reference tmp variable, "tmp" which it passes to the function. The scope of the tmp variable though becomes a problem as it continues to live during the 'd scope.

The value returned by mutate_and_share still counts as a mutable borrow, even though it's an immutable reference, because it's tied to that &mut self borrow that created it. Thus, since loan is holding a mutable borrow on foo, the following foo.share() is blocked. The current error message is pretty good IMO:

error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
  --> src/main.rs:11:5
   |
10 |     let loan = foo.mutate_and_share();
   |                --- mutable borrow occurs here
11 |     foo.share();
   |     ^^^ immutable borrow occurs here
12 | }
   | - mutable borrow ends here

However, NLL will let this example work as-is! It can see the loan is not actually read anywhere, so the associated borrow doesn't really need to continue through the end of its scope.

But if it were instead a borrowed type with a Drop implementation, like some Bar<'c>, then I think NLL still wouldn't allow this. Drop might use whatever was borrowed, and the compiler doesn't do inter-procedural analysis to check what other functions actually use.

1 Like

Thanks for the reply. Good point that the "loan" variable is unused and ignoring it makes the problem go away. Here's a slightly more contrived example that ensures "loan" actually gets used:

fn main() {
    let mut foo = Foo;
    let loan = foo.mutate_and_share();
    foo.output();
    loan.output();
}

error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
   --> src/exp_borrowing.rs:160:5
    |
159 |     let loan = foo.mutate_and_share();
    |                --- mutable borrow occurs here
160 |     foo.share();
    |     ^^^ immutable borrow occurs here
161 |     loan.share();
    |     ---- mutable borrow later used here

Based on something I just read from Niko Matsakis in another thread, the mutate_and_share() function is saying... "if you give me a &mut Foo for the scope called 'c, then I'll give you a &Foo for the remainder of the scope called 'c". If I translate those words into code with an actual tmp variable, I find I get almost the same error message as above!

fn x() {
    let mut foo = Foo;
    let tmp = &mut foo;
    let loan = tmp.mutate_and_share();
    foo.share();
    loan.share();
}

error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
   --> src/exp_borrowing.rs:164:5
    |
162 |     let tmp = &mut foo;
    |               -------- mutable borrow occurs here
163 |     let loan = tmp.mutate_and_share();
164 |     foo.share();
    |     ^^^ immutable borrow occurs here
165 |     loan.share();
    |     ---- mutable borrow later used here

It feels like this experiment supports my mental model that in order to call the function, the compiler is basically creating the tmp automatically at the current scope, which is what leads to the error. Could this be how the compiler, as you say, ties the value returned by the mutate_and_share function to the &mut self borrow that created it?

I get what you're trying to say with the tmp, but I think it's a misleading model. Consider this:

fn main() {
    let mut foo = Foo;
    let loan;

    foo.share(); // ok, temporary immutable borrow
    loan = foo.mutate_and_share(); // stored mutable borrow
    foo.share(); // error, mutable borrow is still active
}

To model this with your explicit tmp, you would have to also declare it early, but not assign it until after the first share call. I guess you could think of it that way, but I doubt it's modeled that way in the compiler. I don't really know the internals of borrowck though, nor the new NLL implementation.

@cuviper Could you explain a bit more what you mean about declaring loan early (or tmp) in your example? For example, this gives almost the same error:

fn main() {
    let mut foo = Foo;
    let loan;

    foo.share(); // ok, temporary immutable borrow

    // This single line ...
    // loan = foo.mutate_and_share(); // stored mutable borrow
    // ... gives almost the same error as these two together...
    let tmp = &mut foo;
    loan = tmp.mutate_and_share();

    foo.share(); // error, mutable borrow is still active
}

Ah, I had started to write my example with an inner block for scoping, but then lost my train of thought why I needed that, so I removed it. So let's try with a block added on your latest example:

fn main() {
    let mut foo = Foo;
    let loan;

    foo.share(); // ok, temporary immutable borrow

    {
        let tmp = &mut foo;
        loan = tmp.mutate_and_share();
    }
    // tmp is gone, but the borrow continues

    foo.share(); // error, mutable borrow is still active
}

In reality, that mutable borrow lasts exactly as long as loan does, which you could model like:

fn main() {
    let mut foo = Foo;
    let (tmp, loan); // same lifetime

    foo.share(); // ok, temporary immutable borrow

    {
        tmp = &mut foo;
        loan = tmp.mutate_and_share();
    }

    foo.share(); // error, mutable borrow is still active
}

But IMO this is no clearer than just training yourself to understand that the borrow lives as long as loan.

And in the NLL world, the borrow only lives until the last use of loan, which you can't really model with some imaginary tmp at all.

1 Like

That's very helpful @cuviper - thanks for the discussion!