Borrow checker and structs containing references

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:

// Hypothetical elided lifetime definition feature (`'*`)
pub struct Token {
    pub ty: TokenType,
    src: &'* [char],
}

What would the meaning of this be now?

    pub fn scan_token(&mut self) -> Result<Token, &'static str> {

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.


  1. 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. ↩︎

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

  3. 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. ↩︎

  4. 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. ↩︎

  5. 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. ↩︎

  6. The more general problem is, you sometimes need a way to override the default meaning of elided parameters. ↩︎

  7. 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. ↩︎

  8. Named always first, named intermixed with elided, ... ↩︎

  9. Depending on the details, it might be a breaking change to go from elided to named or vice-versa too. ↩︎

  10. 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. ↩︎

  11. as best as I was able to discern ↩︎

  12. 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. ↩︎

  13. a reader of the source code wouldn't actually know what's going on unless they knew all the elided types and their bounds ↩︎

  14. And even then only where elision is correct, so not the Token<'a> case for example. ↩︎

  15. "add another lifetime" is one. Here's another. ↩︎

  16. especially when there's a Drop implementation that's definitely going to cause problems ↩︎

  17. More accurately it relies on being able to briefly borrow self exclusively, returning a Token<'a> that remains valid after the exclusive borrow expires. ↩︎

  18. Basically I applied the incorrect annotation from the OP to every method instead of just one. ↩︎

  19. And less often if elided_lifetimes_in_paths was warn or deny by default. ↩︎

2 Likes