The things that make the lifetime annotation syntax awkward for me are

  1. it’s not a real type because it can’t be instantiated like a real type, but it can be passed into the type parameter of a generic type like a real type, and it does have covariant inversion for real subtyping
  2. it can also be used as a type constraint like Trait, in addition to other lifetime annotations like 'a: 'b, it can also be constrained with normal types like T: 'a.

This setting is still acceptable. However, in addition, lifetime appears from time to time together with some other strange syntax, and it is still always scary at this point.

I want to overcome my fear of lifetime by 1. confronting it and 2. organizing and analyzing it.

After roughly sorting out a handful of code I don’t understand, I found that the syntax I don’t understand about lifetime seems to be mainly when it is put together with various generic parameters and even trait constraints.

Find a few examples to look at in detail.

Ref<‘a, T: ‘a>

This example is from https://carols10cents.github.io/book/ch19-02-advanced-lifetimes.html#lifetime-bounds-on-references-to-generic-types

1
struct Ref<'a, T>(&'a T);

This error is said to be reported (but I didn’t reproduce it locally, probably because the rust compiler has added a new omission rule).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
error[E0309]: the parameter type `T` may not live long enough
 --> src/lib.rs:1:19
  |
1 | struct Ref<'a, T>(&'a T);
  |                   ^^^^^^
  |
  = help: consider adding an explicit lifetime bound `T: 'a`...
note: ...so that the reference type `&'a T` does not outlive the data it points at
 --> src/lib.rs:1:19
  |
1 | struct Ref<'a, T>(&'a T);
  |   

Why do we need to add lifetime constraints to the generic parameters here?

Because T may be a structure containing a reference, such as Lexer<'a>, which has the ownership of this Lexer but has external references inside, or it may be a reference type such as &PlainText. In this case, T:'a can be used to declare that the lifetime of the type T must be within the scope of 'a.

Cow<‘a, B: ‘a + ToOwned + ?Sized>

Look at another example of a Cow from the standard library.

1
2
3
4
5
6
7
pub enum Cow<'a, B> 
where
    B: 'a + ToOwned + ?Sized, 
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

Like the previous example, here we have 'a specified for B in the generic parameter to constrain its lifecycle to be within 'a.

The main thing in this code is the ?Sized trait, so let’s look it up.

The Sized trait means that the compiler knows the length of the type, and all type arguments have a default Sized bound. ?Sized means that the bound is released, allowing non-fixed-length types to be accepted. The non-deterministic types are mainly from Slice and Trait Objects, such as dyn MyTrait and [u8]. Non-fixed-length types cannot be stored on the stack, e.g.

1
2
3
4
5
// Can't be stored on the stack directly
struct MySuperSlice {
  info: u32,
  data: [u8],
}

But references to non-fixed-length types are still fixed-length and can be passed on the stack, such as &'a B , &dyn MyTrait , &[u8] . The reason B in Cow<'a, B> allows ?Sized is because the contents of the Borrowed part of the enum are a reference to B. If ?Sized had not been declared, Cow<> would not have been available for [u8].

box_disaplayable<‘a, T: Display + ‘a>

1
2
3
4
5
use std::fmt::Display;

fn box_displayable<T: Display>(t: T) -> Box<dyn Display> {
    Box::new(t)
}

Why does this code not compile?

As in the previous example, the reason is that T could be a structure containing a reference, or it could be a trait that implements a reference type such as impl Display for &MyType. When you move, you need to make sure that it is still in lifetime range after the move.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
error[E0310]: the parameter type `T` may not live long enough
 --> src/lib.rs:4:5
  |
3 | fn box_displayable<T: Display>(t: T) -> Box<dyn Display> {
  |                    -- help: consider adding an explicit lifetime bound...: `T: 'static +`
4 |     Box::new(t)
  |     ^^^^^^^^^^^
  |
note: ...so that the type `T` will meet its required lifetime bounds
 --> src/lib.rs:4:5
  |
4 |     Box::new(t)
  |     ^^^^^^^^^^^

for<‘a>

http://zderadicka.eu/higher-rank/ This example is better, I have a ChuckSum trait wrapped in a calc method with two algorithmic implementations, Xor and Add.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
trait Checksum<R:Read> {
    fn calc(&mut self, r:R) -> Vec<u8>;
}

struct Xor;

impl <R:Read> Checksum<R> for Xor {
    fn calc(&mut self, mut r:R) -> Vec<u8> {
        let mut res: u8 = 0;
        let mut buf = [0u8;8];
        loop {
            let read = r.read(&mut buf).unwrap();
            if read == 0 { break }
            for b in &buf[..read] {
                res ^= b;
            }
        }
        
        vec![res]
    }
}

struct Add;

impl <R:Read> Checksum<R> for Add {
    fn calc(&mut self, mut r:R) -> Vec<u8> {
        // skipped
    }
}

It makes sense. There are several different algorithms, all taking parameters that satisfy the Read trait, to calculate checksum.

The checksum of a file is calculated according to the algorithm specified in the parameter.

1
2
3
4
fn calc_file_with_checksum(_path: String, mut checksumer: impl Checksum<&[u8]>) -> Vec<u8> {
    let buf = "blah blah blah".to_string().into_bytes();
    checksumer.calc(&buf)
}

This will report an error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
╰─$ rustc hrtb.rs                                                                                                                                                                                                                                                1 ↵
error[E0106]: missing lifetime specifier
  --> hrtb.rs:25:73
   |
25 | fn calc_file_with_checksum(_path: String, mut checksumer: impl Checksum<&[u8]>) -> Vec<u8> {
   |                                                                         ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
25 | fn calc_file_with_checksum<'a>(_path: String, mut checksumer: impl Checksum<&'a [u8]>) -> Vec<u8> {
   |                           ++++                                               ++

error: aborting due to previous error

For more information about this error, try `rustc --explain E0106`.

Follow the prompts to change it again.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
error[E0597]: `buf` does not live long enough
  --> hrtb.rs:27:21
   |
25 | fn calc_file_with_checksum<'a>(_path: String, mut checksumer: impl Checksum<&'a [u8]>) -> Vec<u8> {
   |                            -- lifetime `'a` defined here
26 |     let buf = "blah blah blah".to_string().into_bytes();
27 |     checksumer.calc(&buf)
   |     ----------------^^^^-
   |     |               |
   |     |               borrowed value does not live long enough
   |     argument requires that `buf` is borrowed for `'a`
28 | }
   | - `buf` dropped here while still borrowed

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.

Why does the error borrowed value does not live long enough?

Because according to the lifetime constraint, the lifetime of the argument to calc needs to be larger than 'a, but the lifetime of the variable buf is smaller than 'a, so the error is reported.

A reference type can be a type parameter of Generic Trait, which is fine in itself, but a reference requires a lifetime, and the lifetime annotation that has appeared in a function in the past can only come from the declaration of the lifetime generic parameter of the function. If you use a function’s lifetime ‘a to annotate references in generic parameters, you will get the above error.

Using a function’s lifetime ‘a to mark references in Checksum<&[u8]> is not correct from a lifecycle perspective. The lifetime of a reference in Checksum<&[u8]> is only relevant to the point at which it is called, and it will be different for two different calls.

It is possible to do a work around by defining a separate function and ensuring that the lifetime of the reference is consistent through the lifetime tag of the function, and there is no problem.

1
2
3
fn calc_checksum<'a>(buf: &'a [u8], mut c: impl Checksum<&'a [u8]>) -> Vec<u8> {
    c.calc(buf)
}

It would be ugly to wrap a separate function for each similar call point. For this reason rust introduced the HRTB (Higher Rank Trait Bound) syntax, which is this for <'a>. Change it like this.

1
2
3
4
fn calc_file_with_checksum(_path: String, mut checksumer: impl for<'a> Checksum<&'a [u8]>) -> Vec<u8> {
    let buf = "blah blah blah".to_string().into_bytes();
    checksumer.calc(&buf)
}

means that its lifetime is independent of the lifetime marking of the foo function, and is bound specifically at each specific call point, and there is no need to define a separate function just because of lifetime.

Summary

The lifetime marking of structs and functions is relatively easy to understand, but the possibility of substituting reference types for parameters of generic types is one area that is easy to forget. In addition, there is no way to foresee the lifetime of generic parameters when defining a generic structure or Trait, and it is easier to encounter accidents at this time.

Should I add lifetime constraints to generic parameters when defining a structure? The generalized parameters are easy to be mentally defined.

If it is related to a reference, you need to understand that even if the generic parameter is an owned type, there may still be a reference field in it, and it will be related to lifetime. lifetime of the generic parameter satisfies the lifetime constraint of the structure.

Are there any generic parameters that might be passed in by reference in the Trait being used?

It doesn’t make sense to constrain such arguments by the lifetime of the function where they are called; the lifetime they need must be smaller than the function’s lifetime ‘a, and they won’t satisfy the function’s lifetime constraint. In this case, the Trait type itself can be constrained by for<'a>, and the function itself can be used without the lifetime argument.