Lifetime dual syntax question

Consider the following example:

struct Student;
struct Student2;
struct Book<'a>(&'a str);

// This...
impl<'a> Student {
    fn return_book(title: &'a str) -> Book<'a> {
        Book(title)
    }
}

// and this seem to be the same...
impl Student2 {
    fn return_book<'a>(title: &'a str) -> Book<'a> {
        Book(title)
    }
}

Are both these styles conveying the same thing? That title must live for as long as the method that is using it. If so, in the case where Student does not take a generic lifetime... which one should I default too?

Yes, these are the same. I would say that the second one is more idiomatic. Functions within an impl often have different lifetime parameters, so we usually specify them separately for each function.

The elided form is also common:

impl Student {
    fn return_book(title: &str) -> Book<'_> {
        Book(title)
    }
}
1 Like

What these lifetimes really convey is that the argument type &'a str and the return type Book<'a> have the same lifetime. This means that the borrow passed into the function must live as long as the Book that it returns.

Awesome :slight_smile: Learned something new. The elided form looks neat.

There could be a difference if these weren't static methods, but methods on a Student object that use &self. Depending on whether the lifetime is attached to &'a self you can get different behavior.

Slightly simplifying:

  • If the 'a lifetime is invariant, then impl<'a> Student requires thing referenced by 'a to already exist before Student object with that lifetime was created, and the returned Book<'a> will be usable even after Student is dropped. That lifetime is not tied to Student object, but merely "passed through" it.

  • fn return_book<'a>(&'a self, …) starts a new lifetime when you call it, and this lifetime will be tied to the Student object. This allows it to return a reference to something created after the Student was created. That thing is not guaranteed to live longer than Student, so you'd be forced to drop Book<'a> before you're allowed to drop Student.

Neither of those things seem to be true: Playground. Did I misunderstand something?

Perhaps surprisingly, even if we change Student to contain a reference so we have impl<'a> Student<'a>, we don't end up with the first restriction: Playground.

1 Like

Yeah, I've poorly explained it.

For the first case I had this in mind:

use core::cell::RefCell;

struct Student<'a>(RefCell<Option<&'a str>>);
struct Book<'a>(&'a str);

impl<'a> Student<'a> {
    fn return_book(&self, title: &'a str) -> Book<'a> {
        Book(title)
    }
}

fn main() {
    let student = Student(RefCell::new(None));
    let title = String::from("The Raven Tower");
    let book = student.return_book(&title);
    {
        let title2 = String::from("The Raven Tower");
        // suddenly, does not live long enough
        let book2 = student.return_book(&title2); 
    }
    println!("{}", book.0);
}

I must admit, there's more happening there than I expected, because the borrow checker protests when there's RefCell (which could credibly cause use-after-free if this code compiled), but not when there's PhantomData. Is NLL that clever?

2 Likes

For the other case I had this in mind, which is the same as if 'a was elided:

struct Student;
struct Book<'a>(&'a str);

impl Student {
    fn return_book<'a>(&'a self, title: &'a str) -> Book<'a> {
        Book(title)
    }
}

fn main() {
    let student = Student;
    let title = String::from("The Raven Tower");
    let book = student.return_book(&title);
    drop(student); // cannot move out of `student` because it is borrowed
    
    println!("{}", book.0);
}
1 Like

Isn't that just because RefCell<Option<&'a str>> is invariant in 'a whereas PhantomData<&'a str> is covariant?

The invariance forces the two calls to return_book to have the exact same lifetime, so book being alive at the println means this shared lifetime must extend to the println, but the reference passed in the second call cannot live until the println, thus the lifetime error.

With covariance, the two calls to return_book are actually called on values of different types — the first call is on the type Student<'a>, but the second call has student coerced to the subtype Student<'b> for some lifetime 'b shorter than 'a before calling it.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.