Bind function return type to member lifetime

Minimal example: Rust Playground

use std::marker::PhantomData;
use core::ffi::CStr;

struct S<'data, T: AsRef<[u8]> + Clone + 'data> {
    data: T,
    _phantom: PhantomData<&'data ()>,
}

impl<'data, T: AsRef<[u8]> + Clone + 'data> S<'data, T> {
    fn as_str(&self) -> &'data CStr {
        CStr::from_bytes_with_nul(self.data.as_ref()).unwrap()
    }
}

Output:

   Compiling playground v0.0.1 (/playground)
error: lifetime may not live long enough
  --> src/main.rs:11:9
   |
9  | impl<'data, T: AsRef<[u8]> + Clone + 'data> S<'data, T> {
   |      ----- lifetime `'data` defined here
10 |     fn as_str(&self) -> &'data CStr {
   |               - let's call the lifetime of this reference `'1`
11 |         CStr::from_bytes_with_nul(self.data.as_ref()).unwrap()
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ associated function was supposed to return data with lifetime `'data` but it is returning data with lifetime `'1`

error: could not compile `playground` due to previous error

I tried (and failed) to say "The output reference lives as long as the field data, but may outlive self)".
Is there a way to assert this?

It's not possible to return a borrow of a value inside self that outlives self. The reference would be pointing to garbage data if self was dropped while the reference still existed.

IDK, maybe I'm misunderstanding something, but isn't the whole point of naming the lifetime that I can safely reason about these scopes?

Why can I do it with concrete references but not generics?

use std::marker::PhantomData;
use core::ffi::CStr;

struct S<'data> {
    data: &'data [u8],
}

impl<'data> S<'data> {
    fn as_str(&self) -> &'data CStr {
        CStr::from_bytes_with_nul(self.data.as_ref()).unwrap()
    }
}

fn main() {
    let data = [b'a', b'\x00'];
    let cstr = {
        S {data: &data}.as_str()
    };
}

The return of a function must outlive any parameter the function receives.

The difference is that a generic parameter could OWN the data, while an explicit borrow doesn't. For example Vec<u8> implements AsRef<[u8]>, but if you drop S and the reference is still considered valid, the Vec's storage will have been deallocated.

You could create a trait that represents a borrow over a specific lifetime if you wanted a more abstract version of your concrete working example

Playground

use core::ffi::CStr;

// AsRef trait that incorporates a lifetime
trait AsRefLt<'a, T: ?Sized> {
    fn as_ref_lt(&self) -> &'a T;
}

// Implement the trait for all references where the type implements AsRef
impl<'a, T: ?Sized, U: ?Sized> AsRefLt<'a, U> for &'a T
where
    T: AsRef<U>,
{
    fn as_ref_lt(&self) -> &'a U {
        // Dereference self to get the reference with the correct lifetime
        // (the compiler will do this automatically, I'm doing it explicitly for illustration purposes)
        // Since AsRef ties the output lifetime to the input lifetime, we now have a reference with the lifetime we wanted
        T::as_ref(*self)
    }
}

struct S<T> {
    data: T,
}

impl<'data, T: AsRefLt<'data, [u8]> + Clone> S<T> {
    fn as_str(&self) -> &'data CStr {
        CStr::from_bytes_with_nul(self.data.as_ref_lt()).unwrap()
    }
}

fn main() {
    let data = b"Hello, World!\0".to_vec();
    println!("{:?}", S { data: &data }.as_str());
}

If we attempt to implement the new trait for a type which owns it's data we'll run into problems, which is a good sign since that wouldn't make any sense

impl<'a> AsRefLt<'a, [u8]> for Vec<u8> {
    fn as_ref_lt(&self) -> &'a [u8] {
        // No way to get from &self to &'a self here
        &*self
    }
}
error: lifetime may not live long enough
  --> src\bin\main.rs:38:9
   |
36 | impl<'a> AsRefLt<'a, [u8]> for Vec<u8> {
   |      -- lifetime `'a` defined here
37 |     fn as_ref_lt(&self) -> &'a [u8] {
   |                  - let's call the lifetime of this reference `'1`
38 |         &*self
   |         ^^^^^^ associated function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'1`

Note that T: 'data means that the type lives at least as long as 'data, but it doesn't change the lifetimes of any references methods on the type return.

2 Likes
let borrowed = {
    // this works because `Vec<u8>: 'static`
    let s: S<'static, Vec<u8>> = S { data: vec![b'a', b'b', b'c', b'\0'], _phantom: PhantomData };
    // &'static CStr is valid forever
    let cstr: &'static CStr = S::as_str(&s);
    cstr
    // `s` and the contained `Vec<u8>` are dropped here...
};
// ...but we can still access the data from the dropped `Vec<u8>` here -- uh oh
access(borrowed);

Remember: T: 'a only means that a value of type &'a T can soundly exist, not that it's sound to take &'a of any particular value of type T.

3 Likes

No. You name lifetimes to explain to the compiler what you know about how and when varibales are created, borrowed and destroyed.

They don't affect the execution of you program, they are part of built-in proof of correctness which compiler verifies before it run your program.

That one is easy: with concrete implementation compiler implicitly knows more about what is happening, with generics it only knows what you have told it.