Lifetime Issue: Slice of Slices

Hello. I encountered this code example and the corresponding compilation failure. I did not understand why it failed to compile, as it appears that a value is mutably borrowed and then that borrow is explicitly dropped before creating another:

With some experimentation, I was able to find the right combination of labels and bounds for the lifetimes such that the program can compile:

However, I do not understand the problem or the solution. Can anyone explain this in simple terms?

This error is the result of lifetime variance. When you have an &mut T the type T has more constraints on the lifetimes it can accept than in other parts of the language (T is invariant with respect to lifetimes when behind a mutable reference).

Most of the places you can use lifetimes in Rust, the compiler can take the lifetime and say "well it doesn't actually matter exactly what this lifetime is, as long as it lasts long enough". However when you have an exclusive reference &mut T that flexibility is dangerous. I would recommend reading that page in the nomicon, I doubt I'm going to be able to explain variance any more succinctly here [1].

In this specific case &'a mut [&'b mut [f32]] places much stricter constraints on 'b than &'a [&'b mut [f32]] does. When you have &'a mut [&'a mut [f32]] you extend that much stricter bound to the outer reference since both references have the same explicit lifetime. The attempted fix of changing mmul_par to be

fn mmul_par<'a>(c: &'a mut [&'a mut [f32]], a: &[&[f32]], b: &[&[f32]])

introduces a similar problem in the body of that function, which forces the call to split_mut to borrow c for the remainder of the function body.

Essentially by constraining the inner and outer lifetimes to be the same, you tell the borrow checker it can never shorten the lifetime of the borrow, it must use the exact lifetime of the reference you pass to split_mut.

This is a case where not specifying all the lifetimes explicitly like fn split_mut<'a>(a: &'a mut [&mut [f32]]) allows lifetime elision to infer the correct bounds which are much less strict.

The "fixed" playground has fn split_mut<'a, 'b: 'a>(a: &'a mut [&'b mut [f32]]) which is basically what lifetime elision infers AIUI.

TL;DR when you have a lifetime that's behind a mutable reference &'a mut &'b T, you basically never want the mutable reference's lifetime 'a and the lifetime inside the mutable reference 'b to be the same lifetime.


  1. Tries anyway ↩ī¸Ž

3 Likes

Thank you so much. Variance is confusing to me but this helps a lot.