Generic parameter values with different lifetime

Hello everyone, I'm started learning Rust a couple of weeks ago and I'm still trying to wrap my head around some lifetime related stuff. For example, I'm trying to create a basic Buffer that stores primitives values as simple Vectors and slices using a naive Arena approach so I created the following abstraction.

trait Buffer<'a, T> {
    fn push(&'a mut self, value: T);
    fn values(&'a self) -> &'a [T];
    fn clear(&mut self);
}

Implement this trait for primitive values is trivial but for slices like &[u8] we need two different lifetime for the same generic parameter value (&[u8]), one for the values we push into the buffer ( short lifetime because these values are cloned ) and one for getting the values fn values(&'a self) -> &'a [&'a [u8]]

so this is my try:

struct SliceBuffer<'a> {
    buf: Vec<u8>,
    offsets: Vec<usize>,
    values: Vec<&'a [u8]>,
}

impl<'a> SliceBuffer<'a> {
    fn new() -> Self {
        Self {
            values: Vec::new(),
            offsets: Vec::new(),
            buf: Vec::new(),
        }
    }
}

// lifetime 'a for generic parameter value
impl<'a> Buffer<'a, &'a [u8]> for SliceBuffer<'a> {
    // lifetime 'b for the same generic parameter value
    fn push<'b>(&'a mut self, value: &'b [u8]) {
        let start = self.buf.len();
        self.buf.extend_from_slice(value);
        self.offsets.push(self.buf.len());
        self.values.push(&self.buf[start..self.buf.len()]);
    }

    fn values(&'a self) -> &'a [&'a [u8]] {
        &self.values
    }

    fn clear(&mut self) {
        self.buf.clear();
        self.values.clear();
        self.offsets.clear();
    }
}

what puzzles me is why the compiler doesn't complain about the mismatch of lifetimes between the trait specification and the implementation ? There are no guarantees that 'b outlives 'a

There are a lot of interesting things going on here. Let me try to address your main question directly first.

    fn push<'b>(&'a mut self, value: &'b [u8]) {

This implementation is strictly more general than the the trait definition demands, as you declare you can support any lifetime, not just 'a. (You copy out of value, so there's no violation in the body, either.) I believe that this is just a case where the bounds is deferred to the use-site. If you actually try to use a lifetime which is too short, you get an error.

Here's an example of a similar deferral that doesn't involve lifetimes.


Another interesting thing that is happening is that if we expand your push method types, we see that it takes a &'a mut SliceBuffer<'a>. This is a pattern you generally want to avoid. It's saying, "when you call this method, obtain an exclusive borrow for the entire lifetime of the SliceBuffer." See also the example here (search forward for "opposite of the previous example").

If you try to use your compiling code, you will run into this quickly. Once you push, you're stuck; you can't use your SliceBuffer any more.


How to work around that, then? Well, one approach is to implement the trait for all lifetimes smaller than your SliceBuffer:

impl<'slice: 'bufimpl, 'bufimpl> Buffer<'bufimpl, &'slice [u8]> 
    for SliceBuffer<'slice>

But wait... this no longer compiles :frowning:. What went wrong? The errors say the problem is here:

self.values.push(&self.buf[start..self.buf.len()]);

Because this is a SliceBuffer<'slice>, it needs to borrow self.buf for 'slice here. But we only have a &'bufimpl self. The compiler is basically saying "look, you're out of luck -- your implementation as written has to borrow self for 'slice."

Well, can we work around this some other way for SliceBuffer then?


The answer is no, not without changing the fields of SliceBuffer in some major way, or perhaps with unsafe. The core of the problem is that you're trying to create a self-referential struct here, where you're storing references to self.buf in self.values. In fact, the impressive part is that you actually did safely create one! (But it's mutably borrowed forever after the push, and thus not actually useful.)

In short, Rust's ownership and lifetime rules don't work well with self-referential structs. For this to work as intended, you'd need some other approach, like storing offsets, or using raw pointers and unsafe, etc.


Parting remark: lifetimes on traits can be hard to work with, in part because they're invariant. There's nothing on your Buffer trait that really needs the lifetime that I can see. You may want to try continuing with:

trait Buffer<T> {
    fn push(&mut self, value: T);
    fn values(&self) -> &[T];
    fn clear(&mut self);
}

And elsewhere you may end up with an implementation like:

// This implementation is still lifetime bound,
// even though the trait itself has no lifetime parameter
impl<'a, T> Buffer<T> for MyLifetimeParameterizedType<'a, T>

This is another way to avoid the &'lt mut SomeType<'lt> pattern. (It still won't work with your self-referential struct.)

2 Likes

Hello @quinedot , Thank you for this extensive answer, it really helps me to understand these issues with lifetime. I still have a long way to go but after going through all those examples a have a clearer view of the borrow checker and lifetime rules.

Btw, this post --> Common Rust Lifetime Misconceptions should be a must-read for beginners

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.