I'm just learning rust, so maybe this is a dumb question for some reason.
I have some code that does not work:
#![allow(unused)]
struct A {
n: i32,
}
struct B<'a> {
a: &'a mut A,
}
impl Drop for B<'_> {
fn drop(&mut self) {}
}
struct C<'a> {
b: &'a mut B<'a>,
}
fn main() {
let mut a = A{n: 1};
let mut b = B{a: &mut a};
let mut c = C{b: &mut b};
}
The error it gives is:
error[E0597]: `b` does not live long enough
--> src/main.rs:22:22
|
21 | let mut b = B{a: &mut a};
| ----- binding `b` declared here
22 | let mut c = C{b: &mut b};
| ^^^^^^ borrowed value does not live long enough
23 | }
| -
| |
| `b` dropped here while still borrowed
| borrow might be used here, when `b` is dropped and runs the `Drop` code for type `B`
But I don't really understand why this doesn't work. If I add a second lifetime parameter to C like:
struct C<'a, 'b> {
b: &'a mut B<'b>,
}
then it starts working. It seems to me like the &mut b reference that gets passed into c should never be considered to outlive c, but the borrow checker seems to think that it does, and I don't understand why that is.
&'a mut Thing<'a> means "exclusively borrow Thing<'a> for the rest of its validity ('a)", and that includes through when it gets destructed and calls Drop::drop. But Drop::drop requires its own exclusive reference to Thing<'a>. So the call to the destructor conflicts with the &'a mut Thing<'a>.
You can usually get away with a &'a Thing<'a> due to invariance -- If you have a Thing<'long>, you can create a &'short Thing<'short> where 'short is strictly shorter than 'long. This is possible because &'r T is covariant in 'r and in T. So if you force the lifetimes to be the same (&'a Thing<'a>), the compile can still find a solution that doesn't cause the Thing<'_> to be borrowed "forever".
In contrast, &'r mut T is invariant in T, so with whatever &mut Thing<'x> you create referencing a Thing<'long>, 'x == 'long. So if you force the lifetimes to be the same (&'a mut Thing<'a>), then it must have the "borrowed forever" problem. As you found, that's incompatible with having a non-trivial destructor (and with moving, and with borrowing again, etc; basically, &'a mut Thing<'a> is something you never want).
The underlying reason for the invariance is to avoid unsoundness / memory bugs, i.e. Rust's raison d'être. You can use &'a mut Thing<'a> to soundly create self-referencial structs for example... but only due to the "borrowed forever" properties which are too restrictive to be practical.
The passed &mut b reference is of type &'a mut B<'a>. Both of the lifetimes are 'a, which means that the &'a mut A within B<'a> must live the same length as the &mut b. However, the &mut b does not last as long as the &mut a, so it doesn’t compile.
quinedot beat me to explaining &'a mut Struct<'a>.
Ok, this makes sense, and it answers what was going to be my next question, which was going to be "why does it have to expand &a' mut to the lifetime of B's inner &'a mut a rather than shrink the lifetime of B's inner reference to match the outer lifetime?". And the reason for that is because the mutability might allow you to write a short-lived value into a long-lived container.
I'll just throw this out first to perhaps head off some confusion:[1] Rust lifetimes (those '_ things) are generally about the duration of borrows. They do not represent the liveness scope of values or variables. (It's an error for something that is borrowed to go out of scope, but Rust lifetimes and liveness scopes are not the same thing.)
That said, the duration of a borrow and how long you use the reference that created the borrow are very related. References don't have destructors,[2] so they don't cause the borrow (lifetime) to be alive when they go out of scope. However, there are other ways the lifetime can be extended beyond the use of the reference itself. Reborrowing is one, and annotations -- which act like directives to the compiler -- are another.
The outer lifetime is the duration of the borrow of Thing<'a>. The outer 'a and the inner 'a wouldn't be the same lifetime if the borrow could somehow be shorter than the validity of Thing<'a>, and that would be quite confusing. The borrow lasts as long as Thing<'a> is valid because
types containing 'a aren't valid once 'a has "expired", and
the invariance under &mut _ discussed earlier, and
you specifically asked for a borrow as long as 'a by annotating &'a mut Thing<'a> -- by using the same lifetime in both places
The way to annotate "borrow Thing<'a> for only as long as the reference gets used"[3] is to let the outer lifetime be something other than 'a -- &'b mut Thing<'a>.
Or perhaps think of it in terms of generics. If you say
fn unwrap<T>(opt: Option<T>) -> T {
String::new()
}
// or
fn unwrap2<T, U>(opt: Option<T>, u: U) -> T {
u
}
the compiler doesn't go "well you said these types should be the same T, but I guess I'll let the return type be something else like U so the code is useful". By using T in multiple places, you're asking the compiler to enforce that they are the same. Callers of the function can count on this being true. And so it goes with lifetimes too: if you say two lifetimes need to be the same, the compiler will enforce that.
This is a form of reborrowing. The &mut Vec<T> is dropped at the end of the function, but the exclusive borrow of the Vec<T> persists so long as the returned &T is still valid. Any sort of projection[4] works similarly.
That answers the question but probably isn't quite what you meant...
In the context of &'a mut Thing<'a>, your question could perhaps be rephrased, "what's the value of exclusively borrowing Thing<'a> for the rest of its validity?" The compile doesn't know, but it also doesn't care -- that's what you asked for so that's what it's going to enforce!
I actually did give an example earlier -- you can soundly make a self-referencial struct by borrowing forever. It is sound because of the "borrowing forever" -- moving the struct would cause a reference to dangle, taking a &mut _ would cause UB aliasing problems, and so on.
Now, it's still not actually useful -- I've never seen a practical application of &'a mut Thing<'a>.[5] But the compiler doesn't know what's practical or not. I actually find it pretty amazing that the rules of reborrowing, splitting borrows, and equivalent lifetimes allows the sound creation of a self-referencial struct even though there is no self-reference specific logic in the compiler to support it.
I guess what I'm trying to say here is that the compiler is just following the directions you give it and enforcing a set of rules that make things sound, and higher-level outcomes like "borrowed forever" are more emergent properties than deliberate high-level goals. There was a question earlier today that hinged on this difference:
The compiler has no high-level way to know if the OP wanted the lifetime of the Token<'_> to be the same as the borrow of self (the 's on the &'s mut self) or the same as the borrow inside the Scanner<'_> (the 'a in Scanner<'a>). It just enforces what was annotated. In the OPs case, they accidentally annotated[6] "make it the same as 's" when they actually wanted "make it the same as 'a". This caused the compiler to say "hey there's no solution here" elsewhere, leading to the forum post.
In this case changing the annotation to 'a, unifying the lifetime in Scanner<'_> and Token<'_>, was useful. The compiler has no way to know that &'a mut Thing<'a> is not analogously useful. It just knows that's what you said is required.
I think my follow-up question was poorly phrased... probably because I'm struggling to pick the right words out of a large body of brand-new-to-me knowledge.
Your overall explanation that "&'a mut Thing<'a> borrows for 'a because you asked it to" of course makes a lot of sense.
Having thought about it more, I suppose the heart of my follow-up question is less of a question and more of confusion/frustration: I wish struct lifetimes could be elided if they are not "special". "Special" lifetime would mean that the lifetime of the borrow is different than the liveness scope of the containing struct[1]. Basically, if you have no information to add to the lifetime beyond "I named this one 'a and this one 'b" then it seems weird/tedious you have to provide names for them at all, and then explicitly not provide them later (like impl C<'_, '_>).
Requiring a name in the "special" cases makes sense, like that scanner example you linked where Scanner is likely to be discarded but the 'a of its &'a &[char] is relevant to the Token, which may outlive the Scanner (but not outlive the original src). In that case the additional information is "'a is derived from src".
They can be elided in most useful cases. When they cannot be elided, it's because that would cause ambiguities. And requiring the author to disambiguate is a good compromise.
It is already the case that the compiler can infer a shorter lifetime than the liveness of the item. The trick is that it can only do so for shared borrows &T (because they are covariant in T). It is not true for mutable bows &mut T (because they are invariant in T). The reasoning for this rule is important: It enforces the Shared XOR Mutable principle that the language fundamentally depends upon.
Interior mutability is intended to work around limitations associated with this unbreakable rule. These types are based on UnsafeCell and they allow safe mutation through shared references (with their own unique limitations), giving a "best of both worlds" escape hatch for when you really need it. The docs for the typed-arena crate may introduce you to some compelling possibilities in this direction.
This might help soothe your discomfort. The lifetime annotations are just names for generic parameters. Like T in Option<T>. There is little difference: You have to type T to name the parameter; you have to type 'a to name the lifetime. The tick mark ' is an indicator that the "kind" of generic parameter you are dealing with is specifically a region for the borrow checker.
It might seem weird to give it a name in the first place. After all, the compiler is pretty smart, right? It turns out to be useful for distinguishing between types.
"What's the use of their having names," the Gnat said, "if they won't answer to them?"
"No use to THEM," said Alice; "but it's useful to the people that name them, I suppose. If not, why do things have names at all?"
~ Through the Looking-Glass and What Alice found there
Consider Result<T, E>: It would be a much less useful type if it could only be Result<T, T>. But that would hypothetically allow you to elide both parameters entirely because they will always be the same. We will see shortly that this used to be the case for lifetimes in prehistoric Rust, and it was not a good design.
I think a much better introduction is not any forum thread, but the initial motivation for including lifetimes in structs in the first place: Lifetime notation · baby steps[1]
Nico formulates the case better than I have (even though this uses a bikeshed syntax from 2012, the ideas that exist today remain consistent):
What is in fact happening here in terms of the formalism is that the StringReader type has a lifetime parameter named self. In other words, StringReader/&self is a generic type, just like Option<T>, except that it is not generic over a type T but rather over the lifetime self. It is of course possible to have a type like Foo/&self<T>, which is generic over both a lifetime selfand a type T.
One thing which would sometimes be useful (but which is currently unsupported) is the ability to have more than one lifetime parameter on a struct. There are no theoretical reasons for this limitation, it’s simply that the syntax and defaults we’ve adopted didn’t seem to scale up to multiple parameters.
And to wrap up, a few more gems from the same source. These are good for historical context of the original motivation, but they may contain irrelevant and obsolete notions:
I think there's other disconnects between liveness scopes and lifetimes that mean this can't work:
Depending on the exact meaning of "liveness scope" anyway.[1] But, I believe I take your meaning: you want some sort of elision on structs that acts like a fresh lifetime parameter, so you don't have to write it out.[2]
I think this would ultimately be very complicated, confusing, and error-prone in all but the most trivial of examples.
Note that while you can't elide lifetimes on the definition of a struct, you can completely elide them elsewhere already today:[3]
impl<'a> Scanner<'a> {
// `Token` has a completely elided lifetime parameter
pub fn scan_token(&mut self) -> Result<Token, &'static str> {
The elision is actually what caused the poster's error. They needed Token<'a>.[4] Now, let's say they had defined Token like so:
If it's analogous to Token<'_> today, the poster's error still happens.[5] How would you fix it?[6]
Perhaps the fix is, you can still specify these elided parameters if you had to. You'd have to give them a well-defined order, presumably based on their occurrence left-to-right.
But if you could still just specify the parameters, new problems arise. For one, it gives the impression that you can just add more lifetime-carrying fields to a struct without changing the type (by changing the number of parameters), which isn't true. Also, you can't tell if something is borrowing without reading the definition, when looking at the source code.[7] And there are other complications: reordering fields with elided lifetimes becomes a breaking change, because you've also reordered the parameter order. Ordering between named and unnamed lifetime parameters is also something that needs specified. Probably it's confusing to someone no matter how you do that.[8][9]
It would also incentivize independent lifetime parameters for every lifetime, which may or may not be the right thing depending on the use case.[10] In use cases where it's not necessary, it's incentivizing a more complicated data structure. Additionally, of course, there are cases where it's the wrong thing; in some alternate universe, perhaps your OP would have just been asking about one of those cases instead.
Also, if you think borrow check errors are confusing today, imagine if they had to be phrased like "the 2nd elided lifetime of Foo must outlive the 1st elided lifetime of Bar" or such.
There was actually another thread recently where someone wanted[11] something similar for generic type parameters: some way to elide them to make things simpler. Well, visually simpler, anyway. They are of the view that it would make everything simpler. I am of the view that it would make everything (except aesthetics) much more complicated. It has all the same problems as lifetimes in terms of turbofishing, order fragility, and error reporting. Plus it's more important to be able to name generic types for things like additional bounds, qualified paths, and local annotations; you'd be giving those up too. With no always-applicable fix for the bounds, and having to rely purely on inference for the rest.[12] (Unless we get some (probably hideous) syntax for specifying elided types by index or something.)
Just being able to not type out generic parameters (be they lifetimes or types) and have the code maintain the same meaning -- that is not removing any complication from the language, e.g. from the type system or trait solver. The language is just as complicated as it was before. The only thing that gets simpler -- simpler-seeming[13] -- is the raw source code aesthetics.[14]
As soon as you need to know or refer to the elided bits, everything gets more complicated.
So yeah, if you want to define lifetime-using structs, especially with more than one lifetime position within, you have to clear some hurdle in terms of understanding lifetimes and non-trivial borrowing, and your declarations will look relatively more complicated than a non-lifetime struct. But like Scott said in that other thread,
Being able to elide lifetimes at the declaration would make the actual use (and understanding) even harder.
When you're just learning Rust, the practical solution is usually: don't define lifetime-using structs. Newcomers to the language tend to overuse them.
If you're just trying to get your head around lifetimes, that's great. I'm not trying to discourage that at all. Problems like your OP and the solutions[15] are part of that learning process.
Diagnostics and lints could improve to make the situation better. IMO your OP should have triggered a lint about &'a mut B<'a> ("are you sure? The B<'a> is going to be completely unusable...")[16] and suggested a second lifetime. As a human I know &a mut B<'a> isn't useful even if the compiler doesn't, and that's simple enough to lint against.
Finally I'll note that this concept of "special" is very far away from the declaration site! Would people be likely to have the heads-up to realize they needed to name the lifetime somewhere else? What if "somewhere else" is a downstream crate?
Here's a version of the playground where I've removed the use of all non-'static lifetimes outside of the struct declarations. It compiles with no problem... until you get to the commented method scan_all_with_loop, which relies on getting ahold of the inner lifetime instead of the &mut self lifetime.[17][18]
I'm of the opinion that problems such as this would happen a lot more often if you could elide lifetimes at the declaration.[19] It would give the impression that you just don't have to think about lifetimes and it will all just work out. But as it happens, for non-trivial use cases, you do have to think about it.
If you're defining a lifetime-parameterized struct, be prepared to have to think about it.
At one extreme you just end up with a garbage collector. But I think going down the exact meaning of that concept would be a distraction in any case, so I'll say no more about it in this comment. ↩︎
I almost said "like you can in function signatures" here, and that other thread referred to function signatures too, but elided lifetimes in function signatures are actually something quite different. But I think that's also mostly a distraction... ↩︎
Future passive exercise for you: look at some borrow error posts that come up on this forum that supply a playground, and add #![deny(elided_lifetimes_in_paths)] to the playground. See how often one of the error sites is related to the problem/fix. In my experience it's a significant fraction. ↩︎
Alright, I admit it might have still happened if they typed out Token<'_> explicitly. But the ability to elide incentivized the wrong code in this case. If they had been forced to write some lifetime sigil, perhaps they would have caught the mistake. ↩︎
If it's analogous to Token<'a> or to a fresh lifetime variable, other useful code patterns break, so that isn't actually an out to the problem. ↩︎
The more general problem is, you sometimes need a way to override the default meaning of elided parameters. ↩︎
Presumably the rendered documentation would render the parameters -- no change from today! Otherwise, the situation is worse. Knowing that a struct borrows is crucial information. Exactly the sort of thing you need to know when referring to the documentation. ↩︎
Named always first, named intermixed with elided, ... ↩︎
Depending on the details, it might be a breaking change to go from elided to named or vice-versa too. ↩︎
On the other hand, I guess you could say that "less lifetimes" is incentivized today. But I think it's less of an incentive than complete elision would provide. ↩︎
Rust does already have some areas where you must rely on inference and are not given a way to override it, and they can be awful. Closure parameter/return borrowing being the most notorious, probably. ↩︎
a reader of the source code wouldn't actually know what's going on unless they knew all the elided types and their bounds ↩︎
And even then only where elision is correct, so not the Token<'a> case for example. ↩︎
especially when there's a Drop implementation that's definitely going to cause problems ↩︎
More accurately it relies on being able to briefly borrow self exclusively, returning a Token<'a> that remains valid after the exclusive borrow expires. ↩︎
Basically I applied the incorrect annotation from the OP to every method instead of just one. ↩︎
And less often if elided_lifetimes_in_paths was warn or deny by default. ↩︎