We know, Thing borrows Data, that must outlive Thing.
So it's fine to hand-out shared references that are valid
even after Thing goes out of scope
but not longer than Data's scope
Case 2:
Thing holds a mutable borrow of Data.
Access to Data must go through Thing.
Lifetime 'a must be longer that the anonymous lifetime of &mut self.
If that case was allowed, we could hand-out multiple mutable references to Data which is bad
Now I am wondering how Rust thinks about this & accepts Case 1 but comes to the conclusion that Case 2 can cause problems:
22 | impl<'a> Thing<'a> {
| -- lifetime `'a` defined here
23 | pub fn get(&mut self) -> &'a mut Data {
| - let's call the lifetime of this reference `'1`
24 | self.data
| ^^^^^^^^^ method was supposed to return data with lifetime `'a` but it is returning data with lifetime `'1`
I do believe Case 1 is merely solved by the Copy implementation of shared references? So copying a &'a Data gives a &'a Data. Simple enough.
I think to understand that Case 2 error, I first need to understand what self.data is. Is it an (implicit) re-borrow that "somehow" ties the anonymous lifetime of &mut self to &'a mut Data ... but then realizes 'a is not a subtype of the anonymous lifetime?
If 2) was the root-cause of the smartness of the compiler, are there different re-borrow rules for shared and mutable references? Because making Case 1 an explicit re-borrow is still working fine:
Any pointers what documentation or reference explains how Rust actually comes to the conclusion that Case 2 is bad would be highly appreciated as well, Thank You!
Copying, reborrowing, and coercion all kind of occur in the same places and it’s hard to say which one is actually happening, but &T: Copy is sufficient to explain this behavior.
The usual way people explain it is that you are in (with un-elided lifetimes)
pub fn get<'b>(&'b mut self) -> &'a mut Data
such that 'a must outlive 'b, and the rule of exclusive reborrowing is that if you have &'b mut &'a mut Data (ignoring Thing since its existence as an intermediate structure doesn't matter to the situation), you can obtain &'b mut Data from it, but not &'a mut Data.
More precisely, whenever you perform an (implicit or explicit) reborrow, the lifetime of the result cannot be longer than the shortest lifetime among the references that were dereferenced to get to the place you are borrowing.
Sadly, nobody’s written a reference chapter on reborrowing rules.
This seems pretty much correct, in particular the part about not allowing multiple active &mut _ at the same time. That would be undefined behavior. In fact, so is an active &mut _ and an active &_: Once the get method returns, the inner &'a mut Data in the Thing<'a> must be the only reference to the referent.
For Case 1, I might say: Thing<'a> already shared-borrows Data for 'a, so it's okay to create more shared-borrows of duration 'a. (Rust lifetimes like 'a generally are the duration of a borrow.)
In that position, yes, it's interpreted as an attempted reborrow, like &mut *(*self).data.[1] The borrow checker rejects it because you can't reborrow ** &'outer mut &'a mut Data as &'a mut Data,[2] only &'outer mut Data (at most).
The rules stated simply are: Start at the inside and go out. The maximum reborrow duration is the duration of the first shared reference, or if there is none, the outer-most reference.
I don't think reborrows are officially documented at all, sadly. But it is described in the NLL RFC:
That was very helpful! I was afraid I have to go through the NLL RFC to get to the bottom of it; but also was wondering if it's actually describing how things are implemented today. I'll give it a read.
You don't need to read and grok the whole thing to understand why Case 2 errors IMO. The sections on reborrow constraints are accurate in all versions of the borrow checker AFAIK.
On the broader topic of borrow checking, what the RFC describes is not exactly how things are implemented today -- location sensitivity was dropped from NLL due to being too expensive, for example. Problem Case #3 is still with us.
But, there is still a lot of implementation-relevant details and concepts covered in the RFC. It's still useful for building up an explanation of why some things compile, or don't. That said, it's also quite dense and at times subtle. Not really a "read once and aha" type of document (or topic), IMO.
The reference must “go through Thing” accounts for the event that data could relocate due to modification (as in pushing to a vector)? In other words, were it directly to the data the Thing struct could end up pointing to relocated data (UB). Or perhaps I misinterpreted ?
It seems like an issue likely to arise frequently and be puzzling: is there any resource other than the book or the RFC to see some other examples, that experienced users here would recommend to check (and it’s not very advanced) ?
More precisely, whenever you perform an (implicit or explicit) reborrow, the lifetime of the result cannot be longer than the shortest lifetime among the references that were dereferenced to get to the place you are borrowing.
but was this only for exclusive references? As it is not the case in the first example that “works” iiuc.
Yes. My statement was incomplete. As @quinedot said: “Start at the inside and go out. The maximum reborrow duration is the duration of the first shared reference, or if there is none, the outer-most reference.” The reason that stopping in this way is sound is that you can copy that shared reference, at which point that copy exists independently of the place you got it from, so nothing about how you got to it matters.
let x: &'x i32 = ...;
let y: &'y i32 = &*x;
In such cases, there is a connection between the lifetime 'y of the borrow and the lifetime 'x of the original reference. In particular, 'x must outlive 'y ('x: 'y).
This paragraph would explain the mutable case - initial Case 2 - to me… By re-borrowing, you can’t extend the lifetime of a &mut self in a way to return a &’a mut Data .
Until here I was simply accepting the fact that Case 1 - the shared reference case - is just different because shared references are Copy and because that’s “special“ makes the whole thing work.
However, the NLL RFC continues…
In simple cases like this, the relationship is the same regardless of whether the original reference x is a shared (&) or mutable (&mut) reference.
Okay, but ….
However, in more complex cases that involve multiple dereferences, the treatment is different [...] Inituitively, shared references are different because they are Copy – and hence one could always copy the shared reference into a temporary and get an equivalent path.
… this I’d say is the key difference to the initial problem. (Edit: I just realized that @kpreid referenced exactly that sentence from the RFC)
I wasn’t actually dealing with a “simple“ but a multiple-dereferences case, since Case 1 desugars to something like this as you’ve pointed out already:
& *(*self).data
I don’t think I’ve found the “inner to outer“ rule inside the RFC yet but I believe I really have to understand “prefixes“ and some of the other new terminology first.