Lifetime annotation - things that are not clear:

Question1: I see life times noted as 'a, or 'b and sometimes '_

I presume the 'a or 'b are arbitrary names and It seems to be that I can use any letter in the alphabet. and in fact I can use a ascii text word if I want to, except that the word 'static is a special reserved lifetime.

And what is the significance of the '_, I have seen '_' used as a dont care variable name.

Does that mean, we are saying the life time is also don't care?

Question2: Where should lifetimes be annotated?

ie: For example in this: structure declaration

 pub struct FooBuffer<'a> {
     buffer: &'a mut [u8],.
     cursor : usize
 }

First the structure holds a reference to a buffer.

The <'a> attached to the structure name and the buffer reference means that as long as the Structure is valid, the buffer is also valid?

Is that understanding correct?

Question 3 - Sometimes the compiler talks about lifetime 1, and lifetime 2 - but no where in my code do I have a numeric lifetime it is always a letter like: a, b or c

Question 4: Why do I sometimes need to give the impl a lifetime and the struct a lifetime again?

Example, sometimes I need to do this:

 impl<'b>  FooBuffer<'b> {

    pub fn new( buf:  &'b mut [u8] ) -> Self {
        Self {
            cursor : 0,
            buffer : buf,
        }
    }
}

What is the purpose of the 'b on the implementation, what is that telling me? And why do I need another annotation on the Buffer?

Syntactically - it seems that sometimes the lifetime names must match, but other times they do not. Where are the rules that describe the names.? And when they must match other things and when they do not need to match other things.

And why does the lifetimes used for a function argument , in this case the pub fn new() need to match the lifetime of the impl and the Structure name at the start? Where is this described in the documentation?

This is correct, except that both 'static and '_ are special names. The name 'a is commonly used for “the one involved here”. But, in my opinion, once you have more than one lifetime name involved, it’s a good idea to be more specific than 'a and 'b.

_ is not a variable name, and similarly, '_ is not a lifetime name. They both have special meanings.

  • _ is the pattern that does nothing. This is different from an unused/dummy variable; for example, if you do

    let _ = returns_large_or_active_value();
    

    then the return value is dropped immediately because it was not bound to any variable, whereas if you had written let _dont_care = ... it would be dropped at the end of the scope. (But _ doesn’t mean “drop the value”, it means, really, “do nothing with the value”, so the value will just be dropped whenever it would have been otherwise.)

  • '_ means “I am not specifying a lifetime here; let the lifetime elision rules take effect”. It is sometimes syntactically required to give a lifetime name, in which case you need to use '_ or introduce a named lifetime, but for references in particular, there is no difference: &'_ str is exactly the same as &str; they both use the lifetime elision rules

Where they are required or where they make the code clearer. But that doesn’t fit with the next part of your question.

No, the opposite. Borrowing data never keeps that data valid longer than it would otherwise be. What this (required) lifetime annotation means is, roughly, “when the lifetime 'a ends, every FooBuffer<'a> must be gone already”. It constrains FooBuffer to not outlive the thing it borrows.

The compiler uses numbers whenever it has to invent new a lifetime name to talk about how it understands your code. It uses numbers rather than names because they cannot be names in your code. You should understand each one as the compiler saying “if you had used a named lifetime here, then we would be using that name, but since you did not (or could not), we have to make up a new name”.

The way you use the 'b in the implementation in two places — once as FooBuffer<'b> and once as buf: &'b mut [u8] — specifies that those things are using the same lifetime. It specifies that, when someone calls new(), the returned FooBuffer has the same lifetime (is borrowing the same things) as the input &'b mut [u8].

You could write the same function as a free function without any impl:

pub fn new_foo_buffer<'b>(buf: &'b mut [u8]) -> FooBuffer<'b> {
    FooBuffer {
        cursor : 0,
        buffer : buf,
    }
}

This means almost exactly the same thing as the associated function FooBuffer::new. The 'b is explaining what the relationship between the input and output is.

The reason that you have to introduce a lifetime explicitly in the impl, impl<'b>, is because impls can actually have many possible lifetime relationships. For example, you could write impl FooBuffer<'static> in order to define functions that only work with FooBuffer<'static>s. And many more possibilities exist when the impl is a trait impl.

Lifetime names are like variables — you declare a variable and then use it, and similarly, you declare a lifetime name and use it. Generally, if you use a named lifetime exactly once in a function, that is the same as using '_, so most uses for a named lifetime mention it at least twice. In an impl<'a>, that is the declaration, and the use is everywhere else 'a is mentioned.

It never matters what the actual name of a lifetime is — you can always rename a lifetime and have the same program at the end, as long as, just like renaming a variable, you rename the declaration and all of its uses

You can use a completely different lifetime name in the struct and the impl, because those are two different declarations. This is valid:

 pub struct FooBuffer<'asdf> {
     buffer: &'asdf mut [u8],
 }

 impl<'qwer> FooBuffer<'qwer> {
    pub fn new(buf: &'qwer mut [u8]) -> Self {

The reason the 'b in new has to match the 'b in the impl header is because that is how you specify that the FooBuffer is borrowing what buf borrows. Self is just a convenient type alias for the type in the impl header, so this is actually:

 impl<'b> FooBuffer<'b> {
    pub fn new(buf: &'b mut [u8]) -> FooBuffer<'b> {

If you used two different lifetimes in the parameter of new and the return type of new, you would be saying that the returned FooBuffer doesn't borrow from buf, and that would be incorrect and the borrow checker would object.

'_ is usually the same as elision. The main exception is when you have a dyn Trait. In that case, full elision often means dyn Trait + 'static or some other lifetime present in the data type,[1] and using '_ (dyn Trait + '_) makes it act like "normal elision".

Outside of dyn, '_ is particularly useful for cases like

fn method(&self) -> SomeLifetimeParameterizedType<'_>

You use to be able to write that like so without warning:

fn method(&self) -> SomeLifetimeParameterizedType

Nowadays you get a warning, because knowing when a borrow is transferred from &self to the return type is usually critical information that shouldn't be completely hidden.

You have to give them names in data structures.

You should give them names in function signatures when elision isn't correct or sufficient. The canonical "isn't sufficient" case is when you need some lifetimes to be the same:

fn longer<'s>(a: &'s str, b: &'s str) -> &'s str {
    if a.len() >= b.len() { a } else { b }
}

A case where elision is incorrect could be something like

impl S {
    fn method<'s>(&self, s: &'s str) -> Whatever<'s> {
        // ...
    }
}

where the borrow in the return type is related to s and not &self.

You can think of the lifetime here[2] as the duration of a borrow. buffer is an exclusive borrow of some slice of data for that duration. Using the FooBuffer will keep the exclusive borrow of *buffer active. So long as a FooBuffer is usable, the buffer within will also be usable; assuming no UB from unsafe, the compiler prevents things like dangling &mut at compile time.

But so long as *buffer is exclusively borrowed, you can't use it directly, or you'll get a borrow checker error.

Despite the unfortunate naming, Rust lifetimes -- 'a things -- do not directly denote when values are destructed (which is also commonly called a value's "lifetime"). That said, destructing ("dropping") a value is a use that's incompatible with being borrowed.

It's using numbers to denote elided lifetimes. Sometimes naming the lifetimes yourself makes error messages more clear.... provided you don't change the semantics of the code accidentally when doing so. (I.e. you have to understand the elision rules when doing this.)

You need the two lifetimes to be the same, like you need the types to be the same here:

struct FooOwner<T> {
    foo: Vec<T>
}

impl<T> FooOwner<T> {
    pub fn new(foo: Vec<T>) -> Self {
        Self { foo }
    }
}
  • It wouldn't make sense to assign a Vec<i32> when you expected a Vec<String>.
  • It wouldn't make sense to assign a Vec<U> when you expected a Vec<T> (unless U = T).
  • Okay, write some code so that I assign a Vec<T> when I expect a Vec<T> by using T in both places.

Pretty much the same thing is going on with 'b.

You can shorten the borrow duration of a &mut [u8] -- a &'long mut [u8] can coerce to a &'short mut [u8]. But you cannot coerce an arbitrary &'lt mut [u8] to a specific &'b mut [u8]. That's why this does not work:

impl<'b> FooBuffer<'b> {
    // Within this impl block we're working with a particular `'b`.

    // Here you introduce a new, *independent* lifetime `'b2`.
    pub fn new<'b2>(buf: &'b2 mut [u8]) -> Self {
        // You get an outlives error because `&'b2 mut [u8]` cannot coerce
        // to `&'b mut [u8]`.
        Self {
            cursor: 0,
            buffer: buf,
        }
    }
}

Which would be semantically the same as this attempt.

// `impl ... S<'_>` is like `impl<'b> ... S<'b>` (but the lifetime isn't nameable)
impl FooBuffer<'_> {
    // Elision here introduces a new independent lifetime.
    pub fn new(buf: &mut [u8]) -> Self {

This version technically works as well:

impl<'b> FooBuffer<'b> {
    pub fn new<'long: 'b>(buf: &'long mut [u8]) -> Self {

Here new takes a &'long mut [u8] where "'long outlives 'b" -- 'long: 'b. Other ways to think about this are that 'long is valid wherever 'b is (at least), and if something forces 'b to be valid, 'long is forced to be valid too. Unlike before, 'long is not independent of 'b.

This version works because the &'long mut [u8] can coerce to &'b mut [u8]. The rules around what coercions are possible are called variance. Or here's my brief introduction.

But you generally wouldn't actually write the function this way, when equality is sufficient. The consumer of this API isn't going to want the borrow they pass in to be longer than necessary -- longer than the return value is used. And that's all that this more general version does compared to the version that uses equality.

Finally I'll note that Self hides a lifetime in the function signature here.

impl<'b> FooBuffer<'b> {
    pub fn new(buf: &'b mut [u8]) -> FooBuffer<'b> /* <-- Self */ {

I have another section where I go into what signatures like that mean. (Signatures where lifetimes flow from the inputs to the output type.)


  1. e.g. &'a dyn Trait is shorthand for &'a (dyn Trait + 'a) outside of a function body ↩︎

  2. and generally ↩︎

The lifetime labels/annotations are like breadcrumbs to find where the data has been borrowed from. You can look at lifetimes in a function's return type and match it to lifetimes in the function's inputs.

These annotations are restrictions (only shorten and limit usage, never extend). They make structs temporary. They limit where and how the structs can be used. They prevent returning data outside of the scope it's been borrowed from.

The practical meaning of the same lifetime appearing in multiple places depends on whether it's for a shared & or exclusive &mut loan.

For shared, it mostly means that you get the smallest, most restricted scope that allows all of the references to be interchangeable and used at the same time.

For exclusive, it effectively means that all of the loans have to be created at the same time, and can't be any more or less restrictive. That's uncommon. You almost never want to reuse the same lifetime label for more than one reference if there's any &mut involved.