1
votes

My learning process for rust lifetimes looked like this (based on the rust book):

  1. I want to annotate, when values behind references go out of scope
  2. Usually (not always! see .data section, i.e. 'static) values live within a {} block
  3. We annotate blocks like 't: {…} and e.g. struct fields get a lifetime like &'t ident with the same lifetime name t
  4. This understanding is wrong. Why? The block name definitions are most likely unknown to the struct implementor and there might be several block name definitions for the same struct.
  5. So the definition 't: {…} and usage &'t ident must be completely independent.
  6. Compilers can easily determine definitions, thus users never have to write 't: {…}. Programmers only need to care about the &'t ident specification part.
  7. Compilers could analyze function bodies (in case of struct: use of the struct members) and determine the &'t ident part.
  8. This understanding is wrong. Why? Because sometimes the function body (or use of struct members) is not yet available (e.g. a trait specifies a function, but the implementation is done by some other party in the future).
  9. As a result, struct and fn must fully specify lifetimes in their struct definition or function signature, respectively.
  10. Specifications mostly follow the same heuristic rules. So we introduce lifetime elision. It inserts lifetimes based on rules targeting the most common usecases and we can opt-out anytime.

At this point, I think my understanding is pretty close to how it actually works. But now, my understanding gets wrong. Let us look at some example:

#[derive(Debug)]
struct Stats {
  league: &str,
}

const NAME: &str = "rust";

fn more_difficult_league(s1: &Stats, s2: &Stats) -> &str {
  if s1.league == s2.league {
    s1.league
  } else if s1.league == "PHP" {
    s2.league
  } else {
    "C++"
  }
}


fn main() {
  let mut st = Stats { league: name };
  let dleague = more_difficult_league(&st, &st);
  println!("{}", dleague);
}

Obviously, I omitted any lifetime specifications.

  • The lifetime of struct fields is either the entire duration of the program ('static) or as long as the struct (Stats<'a> with league: &'a str)

  • In a function/method, we might get references with lifetimes 'a, 'b, 'c, …. What is the return value's lifetime?

    • Either it is some static value ('static)
    • Either it is always the same specific lifetime (like 'c)
    • Either it is one specific lifetime - which one will be known at compile or run time. For the compiler we must specify the worst case lifetime max('a, 'b, 'c, …). To the best of my knowledge this can be done by giving every reference the same lifetime.

This seems to work for the following contrived, shorter function:

fn more_difficult_league<'a>(s1: &'a Stats, s2: &'a Stats) -> &'a str {
  if s1.league == s2.league {
    s1.league
  } else {
    s2.league
  }
}

If we add some 'static return value, the worst case lifetime is max('a, 'static) which is presumably 'static:

fn more_difficult_league<'a>(s1: &'a Stats, s2: &'a Stats) -> &'static str {
  if s1.league == s2.league {
    s1.league
  } else if s1.league == "PHP" {
    s2.league
  } else {
    "C++"
  }
}

This gives error[E0621]: explicit lifetime required in the type of s1 and lifetime 'static required for s2.league.

At which point is my understanding wrong? Thanks in advance for bearing with me.

Disclaimer: help: add explicit lifetime 'static to the type of s1: &'a Stats<'static> would work here, but seems wrong to me.

1

1 Answers

2
votes

I would change your code as provided below.

Instead of pretending that the result of more_difficult_league() has a static lifetime (which is not the case when we refer to s1 or s2, and the compiler complains about that), we can introduce a new lifetime annotation for this result and specify that the lifetimes of the parameters must outlive this result (the where clause).

#[derive(Debug)]
struct Stats<'a> {
    league: &'a str,
}

const NAME: &str = "rust";

fn more_difficult_league<'a, 'b, 'c>(
    s1: &'a Stats,
    s2: &'b Stats,
) -> &'c str
where
    'a: 'c,
    'b: 'c,
{
    if s1.league == s2.league {
        s1.league
    } else if s1.league == "PHP" {
        s2.league
    } else {
        "C++"
    }
}

fn main() {
    let st = Stats { league: NAME };
    let dleague = more_difficult_league(&st, &st);
    println!("{}", dleague);
}