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.
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.
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 beforeStudent 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.
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?
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.