Practical suggestions for fixing lifetime errors

Does anyone have a reference or tips on how to approach fixing these errors? I've read carefully the Rust Programming Language section on this and have a reasonable understanding of its description. The examples all make sense, but when I have a case that is not as simple as those I'm at a loss of how to address with it (except by posting a request for help here).

Is there some process you go through to analyze the lifelines, or is it something you just look at and see? I know the lifetimes and mutability in my abstract design and it all should work, but clearly do not know how to communicate the design to the compiler.

I could post a link to the code giving me this problem if it would help, but I am most interested in trying to understand in general how to go about analyzing and fixing these problems. People on the forum have been good enough to help with particular examples before, but I don't feel that is getting me any closer to knowing how to deal with these myself.

1 Like

If you link the concrete example I can try to look at it (later today, maybe in 10h) and write down my thought process while finding / approaching the problem.


I would say what's helpful is a combination of many things, including

  • experience with and knowledge of anti-patterns to look for
  • the ability to understand the meaning of lifetimes in type signatures on a more intuitive level and sometimes, if necessary, questioning the API design instead of just chasing compiler errors that may occasionally focus on the wrong end (e. g. caller vs callee) where the truly right fix needs to be done at the other end
  • knowing the tooling necessary of how to reduce or avoid the need for lifetime-parameter rich function signatures, and some ideas of when doing so (e. g. using shared ownership, interior mutability, or the like) is necessary and/or helpful
  • for hard to understand lifetime errors, or often also for questions like "well, if this code doesn't compile, why the hell did that code compile in the first place (and can I make this code more like that code"), it's useful to know more details about language features like variance, coercions, higher-ranked trait bounds, the precise meaning of all cases of lifetime elision (including trait object types), how impl Trait return types interact with lifetimes, how async and futures work, shortcomings and workarounds around type inference of closures, the limitations of the borrow checker and typical examples what polonius can do and how to work around the issue, and so on...
  • for fixing lifetime errors, it's useful to understand the kind of bugs that lifetimes prevent, such as use after free and the like, but also more abstractly unsound APIs[1]. It's a lot easer to fix the "lifetime error" once you identified, intuitively, what the underlying "real" bug was that needs to be fixed; because as long as you don't fix the underlying bug, no amount of shuffling the lifetime signatures will make the error go away

This reminds me also somewhat of the question that every (university) mathematics student will ask themselves a lot when they begin, how do the professors and even the senior students / tutors find these proofs so god damn quickly (often within seconds or at least few minutes) on which I ponder for hours with barely any progress? And the answer is probably mostly just "lots of experience" and "more knowledge + a good intuition on what this knowledge means and how it's applicable"


  1. which ties back into "understanding the meaning of lifetimes in signatures Intuitively" mentioned above, and can be particularly hard since the unsoundness of your API that the rust compiler prevents might only exist in use cases you never intended to have or support in the first place ↩︎

10 Likes

What helped me a lot was to go over the basics, remind myself what “must be” in order to avoid ever having the app point to uninitialized memory. Lifetimes are all about avoiding ever having a ref point to dropped memory. Lifetimes are all about sequencing. It’s obvious stuff, but gave me a solid starting point for working through what can get confusing interpreting the messages from the borrow checker (as good as the messages are).

For instance, you have to initialize values hosted in Wrapper before Wrapper [1] . This implies what needs to “live until”… with that info in hand, how long I needed/could borrow access to that memory. Similarly, any one thing in your complex struct relies on a borrow, the whole thing now can’t live longer than what is being borrowed (all a corollary of the basics required to avoid pointing to bad memory).

Think ahead where you want to own the allocated memory and know when/where it gets dropped. Plan how/when you want to transfer ownership… and what borrows from there etc. need to take “partial” ownership of a struct?, think Option + take (steal the memory and replace with None). This process might help avoid being a slave to the borrow checker as you are going in with a plan ahead of time :)) (I’m smiling b/c it sounds like planning for battle!!).

Others made this point, but there are gaps between what should be ok, and what the compiler can figure out. That is getting better per the Polonius-related comments.

I sometimes find it helpful to name my lifetimes in any non-trivial scenario keeping in mind that more than one lifetime in a struct is rare. This is particularly helpful as it reminds me that a lifetime is tied to an owned memory allocation. Lifetime 'data can be used all over the place, where each borrow with that lifetime tag are used for different amounts of time in (by definition of their different scopes), but all are one in the same in that they can’t live longer than data.

Finally, when figuring out bugs, I have a habit of focusing on the borrowed reference. I have to remind myself that there are in fact two ways to solve the problem of a reference "outliving" the owned memory. The obvious is to shorten the scope of the borrow, but the other is to extend the scope of the allocated memory. That can be done by declaring the variable that will point to the memory in an incrementally larger, more global scope. So separate the declaration from the instantiation/allocation.

Note: Previously I included...

"... where Rust will cast or infer what the user means because it’s there is only one which can work. E.g., &Vec and Vec<&T>… treated as one in the same."

Precisely speaking, I could be off. For the point I'm making, the difference between borrowing a ref to the data (a continuous, typed chunk), and owning refs with borrowing privileges to the data, is more a difference without distinction helped by Rust selecting which IntoIterator to engage based on the borrow, exclusive borrow and ownership. It's further muddied by the fact that Vec's are able to host empty, typed data. So with "blurred" eyes...


  1. even it looks simultaneous, I believe the compiler still goes so far as to generate the nested scopes ↩︎

Thanks, these replies have been very helpful. I've learned there isn't a technical crutch script that can be followed to help organize, that it needs looking and thinking, and will probably be painful at first. I did manage to solve one of the two issues I had with the code, but the second has me stumped.

@steffahn : This is what I tried. I actually have 2 versions of the code written, one of which compiles and one doesn't. In the one that compiles I have a struct

    node: &'a CharsNode,
    string: &'a str,
    match_len: usize,
}

where string is a slice of a string that is passed into the program. It turned out that in some cases I need to back up the string, so I added a struct Matched and used that instead:

pub struct Matched<'a> {
    full_string: &'a str,
    start: usize,
    end: usize,
}
pub struct CharsStep<'a> {
    node: &'a CharsNode,
    matched: Matched<'a>,
}

I don't see any difference in the two structs: in each case there is a slice of a string passed in and 1 or 2 ints. The remaining code is not identical, but very similar, and I can't see why one succeeds while the other has lifetime problems. As a C programmer I know the memory model works, and because the original version compiles and runs I know there should not be an insoluble contradiction in the mutability design.

I broke it down into a simple example that demonstrates the problem. I actually figured out how to compile this (by adding a lifetime 'b to the &self parameter), but when I do that in the actual code there are all sorts of sharing violations.

I'd really appreciate if someone could look and point out to me the difference between these two versions. The code is at GitHub - russellyoung/regexp-rust: Second Rust project: regular expression searcher, with the main compiling and the broken branch with the changes and not compiling. There are workarounds that should be able to avoid this problem, but I'd like to understand why one works and the other doesn't.

I guess my big problem is that this project is probably too complicated for newbie practice. Whenever I work in a new language I try writing a non-trivial program with an algorithm I understand, and usually it works well. While Rust is superficially similar to a lot of other languages some of the concepts are completely new, and it seems to me a background in other languages can almost be a bad thing, since it makes it easy to follow paths that don't fit the Rust model.

Cool, I can probably take a look in about 14 hours and write down my thought process while doing so :slight_smile:

I expect @steffahn's reply will be more in depth and in the spirit of this thread, but on a practical level, try using #![deny(elided_lifetimes_in_paths)] and taking a moment to think about what the lifetime represents whenever it nags you. In order to make sense of things in this case, you'll also need to know or refer to the elision rules. In the playground they mean that these are the same:

impl<'a> Matched<'a> {
    fn next(&self, len: usize) -> Matched { todo!() }
    fn next<'s>(self: &'s Matched<'a>, len: usize) -> Matched<'s> { todo!() }

Compare and contrast with the signature of fn step.


A common newcomer mistake is to overuse references and to try to put them in long-lived structures when they are primarily short-term borrows. Some might level that advice against your crate even, but I feel this is a valid use-case for a lifetime carrying struct. Unfortunately though, by choosing this as one of your learning projects, you've given yourself a bit of a trial by fire by having to deal with lifetime errors before you've built up an intuition for the errors, the elision rules, and so on. I wouldn't beat myself up over it.

8 Likes

I know it sounds easier said than done, but it's about really "getting" the concept of ownership. Once you get an idea of what is possible and what isn't, you design for it, and don't randomly tweak lifetime annotations until they compile. You think "I store this data here, so I can lend it in this scope" or "this will be moved, so I can't lend it, and I'll share it with Arc instead", etc.

Some random things worth knowing to avoid fights with the borrow checker that you can't win:

  • Ignoring the not-so-useful 'static exception, references are always temporary and can't store data. References always borrow from something, so when you have a & you always have to think where's the owner of it stored. You can't make references out of thin air.

  • foo().bar() has different behavior than let tmp = foo(); tmp.bar(). You often need the latter.

  • Structs can't borrow from themselves (they can't be self-referential). You can't use references for parent-child relationships or as a pointer to an element in a collection stored alongside the reference.

  • You can have &mut loans of two different fields of a struct only if you borrow the fields directly, but not via &mut self.

  • Callbacks pretty much always need for<'a> Fn(&'a) syntax.

  • Mutable iterators will likely need unsafe.

  • Iterators can't lend any data they own.

  • &'a mut self is almost always a dead-end that won't work. Don't put lifetimes on &self, and don't put lifetimes in structs and traits unless you're really really sure you know what you're doing.

10 Likes

At a high level, once you chose to ref the string that you are traversing, and want to host the information on the same struct (compound struct in your case), you are asking for trouble.

Unlike other languages, you can’t iterate over “yourself”. Many have discussed how rust disallows self referencing structs. What’s happening here is likely just that: Iterating over self requires reading while writing on self. No bueno. [1].

If I gathered correctly, the node is used to iterate over the full_string.

To record where something happened “along the way”, like all iterators, you have to output something separate from what you are traversing (refs with read/borrow rights are separate). Options might include:

  • only record the start and end, not a ref to full string
  • have a separate collection, eg hashmap, to host the the refs to the full string(s), record a copyable key in addition to the start/end positions??

Other choices, Jon Gjengset (crust of rust on YouTube) has an episode on lifetimes where he provides the rare, super rare, time you might need 2 separate lifetimes… parsing strings. Clearly relevant here :))

Keep in mind, most of these gymnastics that might seem computationally expensive, get peeled away at compile time. Because the compiler can assert so much more about the code (because of the type/lifetime checks), it can get that much more aggressive with the compile time optimizations.

You made the point that given what you know is possible in C, “it should work”. I believe you. Many bump into this issue and are surprised when it happens: it’s not you it’s actually Rust! (How refreshing given how often “the borrow checker was right”). The borrow checker is great but prioritizes safety. That means that if it can’t know for certain, it rejects it. Polonius will have this “false negative” occur less often.


  1. recursive structs are also self referencing and introduce another problem: sizing ↩︎

don't randomly tweak lifetime annotations until they compile.

You've been looking over my shoulder :woozy_face:

I've been there too :slight_smile:

1 Like

Could you explain this? Why is self different than any other variable here? (since it could be passed as Struct::method(&'a mut self, ...) )

I've kind of been assuming if there is impl<'a> Struct {...} that implies a lifetime of 'a* for self.

I had thought this program would have pretty simple memory management - the nodes and steps are mostly immutable once written, except for the most "outer layer" when stepping or backtracking. The data referenced in the structs is a String that never changes. I was beginning to feel as if I was getting it... until I didn't.

!!! My Goodness !!! This fixed the problem - I will have a bit of studying to do to understand why. There is apparently a lot going on that is still beyond my understanding, this was completely off my radar.

Was this something you just saw, or did you try turning off the elided lifetimes to check for a common bug?

It's not self per se, really, it's the implicitness of its type.

&'a mut self (with explicit lifetime) almost always happens when Self is Something<'a>, i.e. the argument is actually self: &'a mut Something<'a>. Then:

  • Struct with lifetime parameter can't live longer then this lifetime (otherwise there would be some dangling reference).
  • References must be assumed to live at least as long as their lifetime parameter.
  • Exclusive (mutable) references are invariant in the type behind them, i.e. when you have &mut Something<'inner>, this 'inner is essentially fixed and can't be neither shortened nor expanded.

Combined, this means that &'a mut Something<'a> borrows the struct until its very destruction. And while this antipattern is more or less obvious with explicitly spelled-out type, it can creep in unexpectedly with self, since type of self isn't there in the signature and can sometimes be not immediately obvious.

2 Likes

The first thing I do with a lifetime problem on an unfamiliar codebase is to flip that lint over to deny, or at least warn, so I can see where the borrows are without having to track down all the types manually. In this particular case, it took me straight to next. At that point it was experience that led me to think "this should almost surely be 'a not '_ (elided)", and indeed just trying that change solved things.

It's nowhere near always that smooth, and so doesn't make the best "how did I solve this" example. This one from earlier today was more involved, but it would take awhile to explore everything. Giving elided lifetimes distinct names is another practical point of advice though, sometimes it improves errors or at least makes them easier to follow than "'_1 would have to outlive '_3". The dead-end of that post is the &'a mut self anti-pattern from above.

If I find some time I'll write up a list of general lifetime things to learn.

@russellw

I'm posting this as promised, writing down thoughts as they come whilst first looking into the code. For this, naturally, as of yet, I haven't read any further conversation in this thread, so there may be observations already made, etc…

Also, this thought process is presumably not 100% natural, because one isn’t usually as thorough normally, and does more “fast” (intuitive) thinking, rather than the ”slow“ (analytical) thinking that’s required when you’re writing down your thought and reasoning at the same time.


So, I'll take a look into the real code first, looking at the error messages in the broken branch to see where the errors are.

The messages themself say

associated function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'1`

which is inherently a harder to understand error message, since it involves an unnamed lifetime, and function signatures…

helpfully the compiler points out what the unnamed '1 lifetime actually is supposed to be, i.e. the lifetime of the &self reference.

405 |     fn step(&self) -> Option<CharsStep<'a>> {
    |             - let's call the lifetime of this reference `'1`

It's important to know how elided lifetimes mean, and what &self means… the desugaring of

fn step(&self) -> Option<CharsStep<'a>> 

is essentially something like

fn step<'elided>(self: &'elided Self) -> Option<CharsStep<'a>> 

where now, 'elided and 'a are two unrelated lifetimes; the compiler complains, presumably, because the actual code does relate those two lifetimes (the '1 lifetime in the error message refers to 'elided now); if this is the case, then the solution would possibly even involve some API-redesign (or fixing, depending on how you see it). But the next step is to look at the code :slight_smile:

pub struct CharsStep<'a> {
    /// The node from phase 1
    node: &'a CharsNode,
    matched: Matched<'a>,
}
impl<'a> CharsStep<'a> {
    fn step(&self) -> Option<CharsStep<'a>> {
        …
    }
}

First thing I notice is that the lifetime 'a is part of the Self type. I somehow overlooked that in the error message. But it makes me think there's some possibility that the type signature may be reasonable, since the data structure does hold references of that lifetime that can reasonably be returned.

It also means that 'elided and 'a are not actually unrelated: The &'elided Self type, i.e. &'elided CharsStep<'a> comes with an implicit 'a: 'elided bound, which means that 'elided is a shorter (or equal) lifetime than 'a, which is however the kind of relation that usually helps relatively little in signatures like the one in question, since the returned type involves the longer lifetime, and a longer-lived reference is naturally “harder” to create[1] than a shorter-lived one.

Finally, in the type I already see some regex-related stuff, and I’m aware that there are some lifetime-related API pitfalls in that crate, due to limitations of the Index trait interface, IIRC; maybe we'll need to come back to this. (Edit: Turns out we won't… but for the sake of having seen it, I was referring to the Captures::get method that can give you a Match<'t> and thus ultimately a &'t str, whereas indexing only gives a shorter-lived one.)

Now, it's time for me to look at the function body:

impl<'a> CharsStep<'a> {
    fn step(&self) -> Option<CharsStep<'a>> {
        if self.matched.end == self.matched.full_string.len() { return None; }
        if let Some(size) = self.node.matches(self.matched.unterminated()) {
            Some(CharsStep {node: self.node, matched: self.matched.next(size)})
        } else { None }
    }
}

In the return value, node: self.node seems unproblematic, since that just copies the reference, preserving the lifetime. On the other hand, Matched::next is a function I'm not familiar with, so I will consult the documentation.

This is when I notice that I have incorrectly thought that Matched might be from a third-party library… maybe I confused it with “Matches” which exists in regex IIRC.

The type signature in question is

impl<'a> Matched<'a> {
    fn next(&self, len: usize) -> Matched { … }
}

This hits a particularly bad corner of the Rust language, in that implicit elided lifetime parameters still don't warn by default… the return type has a lifetime. This should IMO always be written as

impl<'a> Matched<'a> {
    fn next(&self, len: usize) -> Matched<'_> { … }
}

instead so you don't miss the fact there's lifetime. There's a lint already that you can turn on manually, I don’t remember the name off the top of my head. (Edit: looked it up, it’s called elided_lifetimes_in_paths; definitely recommended :slight_smile: )

Anyways, now – also following a special case rule that elided lifetimes in return types, when multiple lifetimes are possible default the the one of the &self/&mut self reference – it desugars to

impl<'a> Matched<'a> {
    fn next<'elided>(&'elided self, len: usize) -> Matched<'elided> { … }
}

That immediately explains how the 'elided lifetime in fn step managed to sneak into the returned value. The obvious solution would be to try rewriting next into the type signature

impl<'a> Matched<'a> {
    fn next<'elided>(&'elided self, len: usize) -> Matched<'a> { … }
} //                                  :-) this changed ----^^

(I'm also basing this on glancing into the definition of Matched and the function body of next for a split second, and it looks like the longer lifetime might easily be supported by the implementation.) Maybe returning Matched<'a> was the intention all along, and the omission of the lifetime was merely an error?

…runing cargo check again… looks like the problem is actually completely addressed this way, as the only remaining errors that come up next are field-privacy-related.


Re-writing this signature back into an elided form gives us

impl<'a> Matched<'a> {
    fn next(&self, len: usize) -> Matched<'a> { … }
}

so the fix was merely adding the 4 characters “<'a>”. Alternatively, writing -> Self would have also avoided the problem. (I've double-checked that adding e.g. pub(super) to the fields in question does indeed result in successfull compilation with cargo check.)


I'd say there was not much thought process involved for fixing this partucular problem, besides identifying relevant type signatures, making sense of what the elided type signatures mean, being aware of the fact that choosing the “correct” lifetime for return types is something to pay attention to, and ultimately fixing something that might have just been a typo, though possibly hard to find due to the lifetime being, unfortunately, very invisible/implicit. I'd be interested in learning more as to which parts of my observation seem the hardest (in a sense of “most unlikely to come up with by yourself”). Cheers :slight_smile:


  1. in the sense that it'll come with more/stricter restrictions the compiler enforces ↩︎

6 Likes

Practical suggestions for building intuition around borrow errors

Ownership, borrowing, and lifetimes covers a lot of ground in Rust, so this ended up being quite long. It's also hard to be succinct because what aspects of the topic a newcomer first encounters depends on what projects they take on in the process of learning Rust. You picked regexes and hit a lot of lifetimes issues off the bat; someone else might choose a web-framework with lots of Arc and Mutex, or async with it's yield points, etc. (Even with how long this is, there are entire areas I didn't even touch on, like destructor mechanics, shared ownership and shared mutability, etc.)

So I'm not expecting anyone to absorb this all at once, but hope it's a useful, broad introduction to the topic. Skim it on the first pass and take time on the parts that seem relevant, or use them as pointers to look up more in-depth documentation, perhaps. In general, your mental model of ownership and borrowing will probably go through a few stages of evolution, and I feel it pays to return to any given area of the topic with fresh eyes every once in awhile.

1. Keep at it and participate

As others have pointed out, experience plays a big role, and it will take some time to build the intuition. This thread, and asking questions here in general, is part of that process. Another thing you can do is to read other's lifetime questions on this forum, read other's replies in those threads, and try playing with the problem yourself.

  • When lost: read the replies and see if you can understand them / learn more
  • When you played with it and got it to compile but aren't sure why: "I don't know if this fixes your use case, but this compiles for me: Playground"
  • After you've got the hang of it: "It's because XYZ, but you can do this instead"
  • Etc

Even after I got my sea-legs, some of the more advanced lifetime/borrow-checker skills I've developed came from solving other people's problems on this forum. Once you can answer other's questions, you may still learn things if someone else comes along and provides a different or better solution.

2. Understand elision and get a feel for when to name lifetimes

Use #![deny(elided_lifetimes_in_paths)] to help avoid invisible borrowing.

Read the function lifetime elision rules. They're intuitive for the most part. The elision rules are built around being what you need in the common case, but they're not always the solution, like this thread has illustrated.

(Namely, they assume an input of &'s self means you meant to return a borrow of self with lifetime 's, versus returning a copy of an inner reference with some longer lifetime.)

If you get a lifetime error involving elided lifetimes, try giving all the lifetimes names. Refer to the elision rules so you're careful not to change the meaning of the signature while you're doing so.

If you end up with a struct that has more than one lifetime... first pause and reconsider, there's a good chance you're overusing borrowing and/or getting into deep water. But if it is actually needed (or you're just experimenting), give them semantically relevant names and use those names consistently.

3. Get a feel for variance, references, and reborrows

Here's some official docs on the topic of variance, but reading it may make you go cross-eyed. Let me try to instead introduce some basic rules in a few layers. If it still makes you cross-eyed, just skim or skip ahead.

3a. A side-note on syntax and implied "outlives" bounds

A 'a: 'b bound means, roughly speaking, 'long: 'short. It's often read as "'a outlives 'b". I also like to read it as "'a is valid for (at least) 'b".

When you have a function argument with a nested reference like &'b Foo<'a>, a 'a: 'b bound is inferred.

3b. A high-level analogy

Some find it helpful to think of shared (&T) and exclusive (&mut T) references like so:

  • &T is a compiler-checked RwLockReadGuard
  • &mut T is an compiler-checked RwLockWriteGuard

You can have a lot of the former, but only one of the latter, at any given time. The exclusivity is key.

3c. The lifetimes of references

  • A &'long T coerces to a &'short T
  • A &'long mut T coerces to a &'short mut T

The technical term is "covariant (in the lifetime)" but a practical mental model is "the (outer) lifetime of references can shrink".

3d. Copy and reborrows

Shared references (&T) implement Copy, which makes them very flexible. Once you have one, you can have as many as you want; once you've exposed one, you can't keep track of how many there are.

Exclusive references (&mut T) do not implement Copy. Instead, you can use them ergonomically through a mechanism called reborrowing. For example here:

fn foo<'v>(v: &'v mut Vec<i32>) {
    v.push(0);         // line 1
    println!("{v:?}"); // line 2
}

You're not moving v: &mut _ when you pass it to push on line 1, or you couldn't print it on line 2. But you're not copying it either, because &mut _ does not implement Copy. Instead *v is reborrowed for some shorter lifetime than 'v, which ends on line 1. An explicit reborrow would look like this:

    Vec::push(&mut *v, 0);

v can't be used while the reborrow &mut *v exists, but after it "expires", you can use v again.

Though tragically underdocumented, reborrowing is what makes &mut usable; there's a lot of implicit reborrowing in Rust. Reborrowing makes &mut T act like the Copy-able &T in some ways. But the necessity that &mut T is exclusive while it exists leads to it being much less flexible. It's also a large topic on its own so I'll stop here.

3e. Nested borrows and the dreaded invariance

Now let's consider nested references:

  • A &'medium &'long U coerces to a &'short &'short U
  • A &'medium mut &'long mut U coerces to a &'short mut &'long mut U...
    • ...but not to a &'short mut &'short mut U

We say that &mut T is invariant in T, which means any lifetimes in T cannot change (grow or shrink) at all. In the example, T is &'long mut U, and the 'long cannot be changed.

Why not? Consider this:

fn bar(v: &mut Vec<&'static str>) {
    let w: &mut Vec<&'_ str> = v; // call the lifetime 'w
    let local = "Gottem".to_string();
    w.push(&*local);
} // `local` drops

If 'w was allowed to be shorter than 'static, we'd end up with a dangling reference in *v after bar returns.

I called invariance "dreaded" because you will inevitably end up with a feel for covariance from using references with their outer lifetimes, but eventually hit a use case where invariance matters and causes some borrow check errors, because it's (necessarily) so much less flexible. It's just part of the Rust learning experience.

Let's look at one more property of nested references:

  • You can get a &'long U from a &'short &'long U
    • Just copy it out!
  • You cannot get a &'long mut U from a &'short mut &'long mut U
    • You can only reborrow a &'short mut U

(The reason is again to prevent memory unsafety.)

3f. Invariance elsewhere you may run into

Cell<T> and RefCell<T> are also invariant in T.

Trait parameters are invariant too. As a result, lifetime-parameterized traits can be onerous to work with. And if you have a bound like T: Trait<U>, U becomes invariant even though it's a type parameter to the trait.

GAT parameters are also invariant.

4. Get a feel for borrow-returning methods

4a. Get a feel for when not to name lifetimes

Sometimes newcomers try to solve borrow check errors by making things more generic, which often involves adding lifetimes and naming previously-elided lifetimes:

fn quz<'a: 'b, 'b>(&'a mut self) -> &'b str { /* ... */ }

But this doesn't actually permit more lifetimes than this:

fn quz<'b>(&'b mut self) -> &'b str { /* ... */ }

Because in the first case, &'a mut self can coerce to &'b mut self. And, in fact, you want it to, because you generally don't want to exclusively borrow self longer than necessary. So instead you can stick with:

fn quz(&mut self) -> &str { /* ... */ }

4b. Bound-related lifetimes "infect" each other

Separating 'a and 'b above didn't make things any more flexible in terms of self being borrowed. Once you declare a bound like 'a: 'b, then the two lifetimes "infect" each other. Even though the return has a different lifetime than the input, it's still effectively a reborrow of the input.

(This can actually happen between two input parameters too: if you've stated a lifetime relationship between two borrows, the compiler assumes they can observe each other in some sense. It's probably not anything you'll run into soon though. The compiler errors read something like "data flows from X into Y".)

4c. &mut inputs don't "downgrade" to &

Still talking about this signature:

fn quz(&mut self) -> &str { /* ... */ }

Newcomers often expect self to only be shared-borrowed after quz returns, because the return is a shared reference. But that's not how things work; self remains exclusively borrowed for as long as the returned &str is valid.

I find looking at the exact return type a trap when trying to build a mental model for this pattern. The fact that the lifetimes are connected is crucial, but beyond that, instead focus on the input parameter: You cannot call the method until you have created a &mut self with a lifetime as long as the return type has. Once that exclusive borrow (or reborrow) is created, the exclusiveness lasts for the entirety of the lifetime. Moreover, you give the &mut self away by calling the method, so you can't create any other reborrows to self other than through whatever the method returns to you.

5. Understand function lifetime parameters

First, note that lifetimes in function signatures are invisible lifetime parameters on the function.

fn zed(s: &str) {}
// same thing
fn zed<'s>(s: &'s str) {}

When you have a lifetime parameter like this, the caller chooses the lifetime. But the body of your function is opaque to the caller: they can only choose lifetimes just longer than your function body.

So when you have a lifetime parameter on your function (without any further bounds), the only things you know are

  • It's longer than your function body
  • You don't get to pick it, it could be arbitrarily longer (even 'static)
  • But it could be arbitrarily shorter than 'static too, you have to support both cases

And the main corollaries are

  • You can't borrow locals for a caller-chosen lifetime
  • You can't extend a caller-chosen lifetime to some other named lifetime in scope
    • Unless there's some other outlives bound that makes it possible

6. Learn some pitfalls and antipatterns

6a. Common Misconceptions

Read this, skipping or skimming the parts that don't make sense yet. Return to it occasionally.

6b. dyn Trait lifetimes and Box<dyn Trait>

Every trait object (dyn Trait) has an elide-able lifetime with it's own defaults when completely elided. The most common way to run into a lifetime error about this is with Box<dyn Trait> in your function signatures, structs, and type aliases, where it means Box<dyn Trait + 'static>.

Often this means non-'static references/types aren't allowed in that context, but sometimes it means that you should add an explicit lifetime, like Box<dyn Trait + 'a> or Box<dyn Trait + '_>. (The latter will act like "normal" elision in function signatures and the like.)

Short example.

6c. Conditional return of a borrow

The compiler isn't perfect, and there are some things it doesn't yet accept which are in fact sound and could be accepted. Perhaps the most common one to trip on is conditional return of a borrow, aka NLL Problem Case #3. There are some examples and workarounds in the issue and related issues.

The plan is still to accept that pattern some day.

If you run into something and don't understand why it's an error / think it should be allowed, try asking in a forum post.

6d. Avoid self-referential structs

By self-referential, I mean you have one field that is a reference, and that reference points to another field (or contents of a field) in the same struct.

struct Snek<'a> {
    owned: String,
    // Like if you want this to point to the `owned` field
    borrowed: &'a str,
}

The only safe way to construct this to be self-referential is to take a &'a mut Snek<'a>, get a &'a str to the owned field, and assign it to the borrowed field.

impl<'a> Snek<'a> {
    fn bite(&'a mut self) {
        self.borrowed = &self.owned;
    }
}

And as I believe was covered earlier in this thread, that's an anti-pattern because once you create a &'a mut Thing<'a>, you can never directly use the Thing<'a> again. You can't call methods on it, you can't move it, and if you have a non-trivial destructor, you can't (safely) make the code compile at all. The only way to use it at all from a (reborrowed) return value from the method call that required &'a mut self.

So it's technically possible, but so restrictive it's pretty much always useless.

Trying to create self-referential structs is a common newcomer misstep, and you may see the response to questions about them in the approximated form of "you can't do that in safe Rust".

6e. &'a mut self and Self aliasing more generally

This thread has already covered how &'a mut Thing<'a> is an anti-pattern which is often disguised in the form of &'a mut self.

More generally, self types and the Self alias include any parameters on the type constructor post-resolution. Which means here:

impl<'a> Node<'a> {
    fn new(s: &str) -> Self {
        Node(s)
    }
}

Self is an alias for Node<'a>. It is not an alias for Node<'_>. So it means:

    fn new<'s>(s: &'s str) -> Node<'a> {

And not:

    fn new<'s>(s: &'s str) -> Node<'s> {

And you really want one of these:

    fn new(s: &'a str) -> Self {
    fn new(s: &str) -> Node<'_> {

Similarly, using Self as a constructor will use the resolved type parameters. So this won't work:

    fn new(s: &str) -> Node<'_> {
        Self(s)
    }

You need

    fn new(s: &str) -> Node<'_> {
        Node(s)
    }

7. Scrutinize compiler advice

The compiler gives better errors than pretty much any other language I've used, but it still does give some poor suggestions in some cases. It's hard to turn a borrow check error into an accurate "what did the programmer mean" error. So suggested bounds are an area where it can be better to take a moment to try and understand what's going on with the lifetimes, and not just blindly applying compiler advice.

I'll cover a few scenarios here.

7a. Advice to change function signature when aliases are involved

Here's one of the scenarios from just above. The compiler advice is:

error[E0621]: explicit lifetime required in the type of `s`
 --> src/lib.rs:5:9
  |
4 |     fn new(s: &str) -> Node<'_> {
  |               ---- help: add explicit lifetime `'a` to the type of `s`: `&'a str`
5 |         Self(s)
  |         ^^^^^^^ lifetime `'a` required

But this works just as well:

-        Self(s)
+        Node(s)

And you may get this advice when implementing a trait, where you can't change the signature.

7b. Advice to add bound which implies lifetime equality

The example for this one is very contrived, but consider the output here:

fn f<'a, 'b>(s: &'a mut &'b mut str) -> &'b str {
    *s
}
  = help: consider adding the following bound: `'a: 'b`

With the nested lifetime in the argument, there's already an implied 'b: 'a bound. If you follow the advice and add a 'a: 'b bound, then the two bounds together imply that 'a and 'b are in fact the same lifetime. More clear advice would be to use a single lifetime. Even better advice for this particular example would be to return &'a str instead.

This class of advice can also be relevant because by blindly following the advice, you can end up with something like this:

impl Node<'a> {
    fn g<'s: 'a>(&'s mut self) { /* ... */ }

And that's the &'a mut Node<'a> anti-pattern in disguise! This will probably be unusable and hints at a deeper problem that needs solved.

7c. Advice to add a static bound

The compiler is gradually getting better about this, but when it suggests to use a &'static or that a lifetime needs to outlive 'static, it usually actually means either

  • You're in a context where non-'static references/types aren't allowed
  • You should add a lifetime parameter somewhere

Rather than try to cook up my own example, I'll just link to this issue. Although it's closed, there's still room for improvement in some of the examples within.

8. Circle back

Ownership, borrowing, and lifetimes is a huge topic. There's way too much in this "intro" post alone for you to absorb everything in it at once. So occasionally circle back and revisit the common misconceptions, or the documentation on variance, or take another crack at some complicated problem you saw. Your mental model will expand over time; it's enough in the beginning to know some things exist and revisit them when you run into a wall.

(Moreover, Rust is practicality oriented, and the abilities of the compiler have developed organically to allow common patterns soundly and ergonomically. Which is to say that the borrow checker has a fractal surface; there's an exception to any mental model of the borrow checker. So there's always something new to learn, forget, and relearn, if you're into that.)

20 Likes

This is wonderful, exactly what I was hoping for. I expect it will keep me busy for some time trying to understand and appreciate all of it.

Is there any way of marking it to make it easier for others to find? The guidance and suggestions in it should be of use to anyone starting out. Before asking here I googled but was unable to find this type of help.

I've uploaded a version of that post here.

6 Likes

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.