"Advanced" Lifetimes Question


#1

If my question is prompted by the “Advanced Features” section of the book, it must be “advanced”, right…?

I took the example that ends on page 428 of The Book and added a little extra, to see run it (see what gdb thinks, now that I figured out how to set a breakpoint in my code), make the reality real, so it will all be clear. It isn’t clear.

I have this.

In main.rs:

#![feature(extern_prelude)] // Compiler told me to put this here. 
                            // Donno what it does.
fn main() {
    let y = lifetimes::try_it();
    println!("y: {:?}", y);
}

In lib.rs:

struct Context<'s>(&'s str);

struct Parser<'c, 's: 'c> {  // s lives at least as long as c
    context: &'c Context<'s>,
}

impl<'c, 's> Parser<'c, 's> {
    fn parse(&self) -> Result<(), &'s str> {
        Err(&self.context.0[1..])
    }
}

fn parse_context(context: Context) -> Result<(), &str> {
    Parser { context: &context }.parse()
}

pub fn try_it<'a>() -> Result<(), &'a str> {
    let x = Context("def");

    let y = parse_context(x);

    y
}

When I run it I see:

y: Err("ef")

Cool. But why did it work? Where is that “ef” sitting? When I called try_it(), didn’t x get allocated on the stack inside try_it()? By the time I print it in main(), isn’t it long gone? Where is my “ef”?

Thanks,

-kb, the Kent who is convinced that lifetimes will be clear as a bell, any second now, for sure Rocky.


#2

“def” being a “str” literal is an &str (string-slice) with a 'static lifetime (all str literals are compiled statically into the final binary). Does that give you a hint?


#3

A hint. But also a growing suspicion these lifetime annotations are just make-work.


#4

Second hint. When you instantiated the Context struct, the lifetime of the parameter passed to it was 'static, so 's is 'static. Follow it through…

What if the thing you gave it wasn’t a static life-time? Would it be make-work then?


#5

Lemme think…


#6

Allow me to shift to a different question.

When I say:

'a

How long does anyone care about my choice of letter? For the span of that source file? Something smaller? Because when I say:

impl<'c, 's> Parser<'c, 's> {

That first <> feels like it holds a declaration and the second <> feels like it holds a use. But in this case they both seem to have refer to lifetimes designators that have been mentioned a couple lines above.

-kb, the Kent who feels like he understands the problem of lifetimes but is confused by these dang tick marks that are supposed to fix things.


#7

No no, they don’t refer anything. Basically, you can think of lifetime designators as regular generic type arguments, with the exception that they are used for checking the borrowing rules by the compiler, and they are stripped away from the resulting code.

If you have a struct

struct Hello<'a> {
    name: &'a str
}

you can follow with

impl<'whatever> Hello<'whatever> {
    ...
}

The latter is required because the struct requires it (holds a use), the former is a declaration of a lifetime (holds a declaration).

So the 'a only exists for the struct, and 'whatever is only defined for the impl. Does this help you?


#8

I think I am getting closer.

Where can a lifetime identifier make its first appearances, and how long does it have any meaning? Just per block {}?

Why does 'whatever appear twice? The first one feels like a declaration. The second a use.


#9

For structs, in angle brackets after a type name:

struct Hello<'a> {
//           ^^
//           the first appearance, defines a lifetime, may be arbitrary named
    name: &'a str
//         ^^
//         then, you use the designator defined above
}

Now you want to add some functions for this struct. You can’t say

impl Hello { ... }

because Hello requires a lifetime. Let’s rewrite:

impl Hello<'b> { ... }
//         ^^
//         may be arbitrary named. I'm using a different name to show you
//         that it doesn't have to be 'a from the struct declaration above

Now the compiler complains that the 'b thing is not defined. Let’s define it:

impl<'b> Hello<'b> { ... }
//   ^^
//   defines a lifetime

Now it’s OK.


Yep!

Exactly.


#10

Consider an example with type arguments instead of lifetimes:

enum Option<T> {
    Some(T),
    None
}

impl<Whatever> Option<Whatever> {
    pub fn to_inner(self) -> Whatever {
        match self {
            Option::Some(inner) => inner,
            Option::None => panic!("to_inner called on Option::None")
        }
    }
}

It works the same way: Whatever doesn’t refer to T. Instead, they both refer to some (but the same) type:

fn main() {
    // opt is Option<u32>
    let opt = Option::Some(123u32);
    
    // to_inner is Option<u32> -> u32
    // even through `Option` was declared as Option<T>,
    // and `to_inner` as to_inner(self) -> Whatever
    assert_eq!(opt.to_inner(), 123);
}

#11

That makes the syntax and scope questions clear. Thanks.

I think need to revisit this “advanced” question in the morning, think through the “lives at least as long as” part.

Thanks,

-kb


#12

I just wanted to tell you thank you for this simple but precise explanation. I feel aggrandized in my Rust knowledge… :sunglasses: