Lifetime Elision 'a

i Try to add lifetime with 'a it comes when i ad from left to right get no problem if im explore it with only return have 'a comes with undeclared lifetime.

please some one can explain what rustlang do with lifetime checking rules.

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

    // first_word(s: &str) -> &str {}           | OK
    // first_word<'a>(s: &str) -> &str {}       | OK
    // first_word<'a>(s: &'a str) -> &str {}    | OK
    // first_word(s: &str) -> &'a str {}        | undeclared lifetime
    // first_word(s" &'a str) -> &str {}        | undeclared lifetime and expected named lifetime parameter

    let bytes = s.as_bytes();

    for (i , &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i]
        }
    }

    &s[..]
}

fn main() {

    let my_string = String::from("hello world.");

    let word = first_word(&my_string[..]);
    println!("value my_string: {}", word);

    let my_string_literal = "hello world.";

    let word = first_word(&my_string_literal[..]);
    println!("value &my_string_literal[..]: {}", word);

    let word = first_word(my_string_literal);
    println!("value my_string_literal: {}", word);

}

If you use a named lifetime like 'a, you must first declare it.

fn first_word<'a>(s: &'a str) -> &str { ... }
//            ^^      ^^---used
//            ||---declared

So:

fn first_word(s: &str) -> &'a str { ... }
//                         ^^---Error, no <'a>
fn first_word(s: &'a str) -> &str { ... }
//                ^^---Error, no <'a>

In some cases, you don't have to use a named lifetime like 'a. More details are in the nomicon.

Thus:

fn first_word(s: &str) -> &str { ... }
// Same as: first_word<'a>(s: &'a str) -> &'a str { ... }
fn first_word<'a>(s: &'a str) -> &str {}
// Same as: first_word<'a>(s: &'a str) -> &'a str { ... }
fn first_word<'a>(s: &str) -> &str {}
// Technically the same as: first_word<'a, 'b>(s: &'b str) -> &'b str { ... } // ('a is unusued)

If there is no &self or &mut self and more than one input lifetime, every output lifetime needs a named lifetime:

some_func<'a, 'b>(s: &'a str, t: &'b str) -> &str { ... }
//                                           ^---Error: Must name lifetime
3 Likes

thank you, i never read rust nomicon just learn rust from rustup doc --book. nomicon more detailed and more explanation.

many thanks @quinedot

Also, once a generic parameter is introduced, such as a generic lifetime parameter (e.g., <'a>), then, like any other parameter, specific choices of the "value" of that parameter are made by the caller. So, when writing a generic function's signature, you have to think you (the callee) don't control the generic parameters.

What you, the callee, control, is the rest of the function signature, that is, the contract expressed by that signature, and the constraints that go with it.

So, if you write:

fn first_word<'a> (s: &'_ str) -> &'a str
  • (&'_ str is the notation to explicitly elide a lifetime parameter (&str is sugar for that))

  • Unelided signature
    fn first_word<'a, 'elided1> (s: &'elided1 str) -> &'a str
    

then, what that signature is saying is that the return value is a borrow tied to the caller-provided 'a lifetime, whatever that 'a may be. The caller could, for instance, provide 'a = 'static, since _there is no constraint preventing them to do so. So, in this case, that signature is equivalent to:

fn first_word (s: &'_ str) -> &'static str

(and if you tried to implement any of those two signatures, you will notice that you will "only" be able to return a string literal "…" (and that you cannot return s)).

So, the trick / the idea to keep these caller-provided parameters under control / under check is to constrain those by binding those lifetime parameters to actual function parameters, by tying those together, by using a generic lifetime parameter with a function argument:

fn first_word<'a> (s: &'a str) ...
//            ^^   ^^^^^^^^^^
//          this       |
//       parameter…    |
//                  … is bound to
//                    this argument

This means that while the caller could still choose to pick something like 'a = 'static; this time, however, they would also have to be providing an s: &'static str that goes with it. More generally, any lifetime 'a that they choose would have to be the lifetime of the borrow of *s, (or a subset of it), which in practice boils down to "'a is now the lifetime of the input borrow".

This is how reasoning with lifetimes works: by using an initially "free" lifetime parameter (such as <'a> in the example) in one (or more!) inputs, such parameter must now become "the lifetime of that input (or of those inputs)". This, in turn, allows to then use such a lifetime parameter in return position to, this time, tie the lifetime of your returned value to that very lifetime.

fn first_word<'a> (s: &'a str) -> &'a str
//            ^^   ^^^^^^^^^^     ^^^^^^^
//          this       |              |
//       parameter…    |              |
//                  … is bound to     |
//                    this argument…  |
//                                … and the return value
//                                  is also tied to that
//                                  lifetime parameter:
//                                  the return value is thus
//                                  bound to that first argument,
//                               "the return value borrows from s"

This, in general, can be hard to understand, because the lifetime parameters are very badly named in the book.

Since, as I mentioned before, a lifetime parameter ought to be used in some function argument, I find it quite useful to have that lifetime parameter be named as that function argument:

fn first_word<'s> (s: &'s str) -> &'s str
  • This is the same signature as the one just above it, and yet better conveys that the return value has the lifetime of its s argument.

Let's see a more interesting example, the needle and haystack problem: we have two strings, a needle and a haystack, and we would like to know if the haystack contains the needle (i.e., if the latter is a substring of the former), and if so, to get the (first) position of the needle inside the haystack:

fn f<'haystack, 'needle> (
    haystack: &'haystack str,
    needle: &'needle str,
) -> Option<usize> // optional index
{
    // naive implementation:
    let mut char_indices = haystack.char_indices();
    loop {
        let remaining = char_indices.as_str();
        if let Some((pos, _)) = char_indices.next() {
            if char_indices.as_str().starts_with(needle) {
                return Some(pos);
            } // else { continue; }
        } else {
           return None;
       }
    }
}

Now, that works, but what if, instead of returning just a position, we returned a borrow from the haystack string? In that case, the signature ought to express that the return value borrows from the haystack, and not the needle. Well, this is easy, we just have to apply the ideas discussed here:

  • What does the return value borrow?

    • If it does not borrow any input, then it shouldn't have any lifetime parameter (besides 'static), and you can leave all the input lifetime parameters elided;

      Example:

      fn greet (casual: &'_ bool) -> &'static str
      {
          if *casual { "Hi!" } else { "Greetings." }
      }
      
    • If it borrows from exactly one input, then introduce a lifetime parameter named as that input, and use that lifetime in the return type;

      This is our case here:

      //              'needle could be kept elided
      fn f<'haystack, 'needle> (
          haystack: &'haystack str,
          needle: &'needle str, // `&'_ str` or `&str`
      ) -> Option<&'haystack str>
      
    • if it may borrow from multiple inputs, then all these inputs will need to be using the same lifetime parameter, and you can name that lifetime 'ret, or with the function name itself.

      An example:

      fn smaller<'ret> (a: &'ret str, b: &'ret str)
        -> &'ret str
      {
          if a <= b { a } else { b }
      }
      

Finally, an interesting more advanced lifetime example, is the .as_str() method on CharIndices I have used in my haystack example: as you can see, the CharIndices<'a> itself contains that lifetime parameter <'a> which is actually the lifetime 'char_indices of the initial call, i.e., the lifetime of the source 'string (all these are valid and better names than 'a, so use whichever you prefer):

impl<'orig_string> CharIndices<'orig_string> {
    fn as_str<'as_str_call> (
        self: &'as_str_call CharIndices<'orig_string>,
    ) -> &'orig_string str
    ...
}

As you can see, with lifetimes, we have been able to express the more advanced notion that the yielded string is not directly borrowing the CharIndices instance itself, but rather, the original string (so that this CharIndices instance can be trashed, moved, mutated, that the obtained .as_str() will remain valid).

3 Likes