Lifetime Constraints on Slices of Slices

Hey everybody, how come I can take a slice of a slice and then let the slice-slice outlive the slice? Like why does this compile:

fn slice_of_slice<'pool, 'slice, 'ret>(
    pool: &'pool [usize],
    slice: &'slice [usize],
) -> &'ret [usize]
where
    'pool: 'slice, // `pool` outlives `slice`
    'slice: 'ret,  // `slice` outlives the return value
{
    // This is some ridiculous code meant to ensure both parameters are used.
    let pool_idx = slice[0];
    let slice_idx = pool[pool_idx];
    &slice[slice_idx..slice_idx + 1]
}

#[test]
fn test_slice_of_slice() {
    let pool = vec![1, 2, 3, 1, 2, 99, 3, 1, 1, 1, 2, 3, 4, 2];
    let ret = {
        let slice = &pool[3..6]; // == &[1, 99, 2]
        slice_of_slice(&pool, slice)
    }; // `slice` is dropped here.
    // `ret` is still alive.
    assert_eq!(ret, &[99]); // I thought `slice` had to outlive `ret`!
}

I thought that specifying those lifetime constraints would prevent the test code from compiling, but it does compile! Anybody have an explanation?

Lifetimes say how long a reference can live, not how long it must live. You can store a long-lived borrow into a short-lived variable (like the borrow that is stored in slice here); you just can't do the opposite.

And in general, dropping a reference doesn't have any effect—especially with shared references, which are Copy. The slice parameter in the slice_of_slice function is an independent copy of the slice variable in the test function.

1 Like

To add to this a little: The variable being borrowed (the referent, in this case pool) must live at least as long as the duration of the borrow. The variable that holds the borrow (the reference, in this case slice) must live at most as long as the duration of the borrow.

1 Like

It's not slice that has to outlive the returned value; it's the data that slice points to. This is fine because slice points to data stored in pool, which does indeed outlive the returned data.

1 Like

Ohhhh! Thank you!

If that's the case, do you think Rust made a similar mistake to C's pointer star placement where char *f() is often better thought of as char* f()? Would it be helpful for me to think of &'a [u8] as & 'a[u8] i.e. the [u8]-buffer is what has the 'a lifetime?

No, I don't think that it is the same situation here. How long the reference can live is part of the reference, and that is how it should be. It would have been a comparable mistake if the 'a was somehow tied to the variable name, not the reference. The variable can just contain a value that lives longer than the variable itself.

In fact there isn't any requirement that 'a is the full lifetime of the data it points to. It can be shorter than that. You might like this post of mine.

1 Like