1

Here is a simple example of where you only need to specify the lifetime for a mutable reference:

fn foo<'a>(x: &'a [&'a i32]) -> impl Iterator<Item = ()> + 'a {
    x.iter().map(|_| ())
}

fn foo_mut<'a>(x: &'a mut [&'a i32]) -> impl Iterator<Item = ()> + 'a {
    x.iter().map(|_| ())
}

fn bar(x: &[&i32]) {
    for _ in foo(x) {}
}

// why is an explicit lifetime needed?
fn bar_mut<'a>(x: &'a mut [&'a i32]) {
    for _ in foo_mut(x) {}
}

I would like to not have to specify these lifetimes because it make it much more difficult to call the function.

Is this just some compiler bug or am I missing something?

You can work around this by using a macro but I would really like to be able to define this as a function without specifying the lifetime.

Drew Smith
  • 73
  • 6

1 Answers1

4

Firstly, lets talk about lifetime elision. All references have some lifetime associated with them, but Rust allows you to elide the annotation when it can appropriately inferred.

If you were to write:

fn bar_mut(x: &mut [&i32]) {
    for _ in foo_mut(x) {}
}

The compiler would infer each reference to have its own lifetime:

fn bar_mut<'a, 'b>(x: &'a mut [&'b i32]) {
    for _ in foo_mut(x) {}
}

As you can see, the signature of bar_mut is different than the signature of foo_mut; the lifetime of the slice, &[_] doesn't have to match the lifetime of its elements, &i32, so it can't satisfy the constraints set by foo_mut.


So why does it work for foo and bar?

Second, lets talk about variance. Lifetimes in particular can be adjusted by the compiler (according to their variance) usually to the minimal frame to which they are needed. Typically, references' lifetimes are called covariant which means the compiler can shorten them as needed.

If we look at bar with explicit lifetimes:

fn bar<'a, 'b>(x: &'a [&'b i32]) {
    for _ in foo(x) {}
}

The 'b lifetime is shortened to match 'a when passing x to foo and it can do that because 'b is covariant. There is also an implied 'b: 'a constraint due to the construction of nested references which makes this guaranteed to be valid but that's not important at the moment.

However, mutable references are a bit more strict since their content can be re-assigned. The lifetime of a mutable reference is still covariant, but the type it references must be invariant. Otherwise you would be able to assign via that reference a value that lives shorter than it actually needs. So if we look at your signature for foo_mut:

fn foo_mut<'a>(x: &'a mut [&'a i32]) -> ...

The lifetime associated with the slice, &mut [_], can be covariant but the lifetime of the element, &i32, must be invariant because it is behind a mutable reference. Thus 'a is invariant. So when we look at bar_mut with different lifetimes:

fn bar_mut<'a, 'b>(x: &'a mut [&'b i32]) {
    for _ in foo_mut(x) {}
}

The lifetime 'b is likewise invariant so it stays the same when passing to foo_mut. And since foo_mut requires the lifetimes to be the same, 'a must match 'b. And 'b cannot be shortened, so 'a must be extended but that is not allowed since 'a is only covariant (i.e. it can only be shortened). This is how you get the error:

error: lifetime may not live long enough
  --> src/lib.rs:15:14
   |
14 | fn bar_mut<'a, 'b>(x: &'a mut [&'b i32]) {
   |            --  -- lifetime `'b` defined here
   |            |
   |            lifetime `'a` defined here
15 |     for _ in foo_mut(x) {}
   |              ^^^^^^^^^^ argument requires that `'a` must outlive `'b`

I would really like to be able to define this as a function without specifying the lifetime.

You're only in this pickle because foo and foo_mut are needlessly over constrained. If you annotate the lifetimes to only what is needed, you don't have this issue at all:

fn foo<'a>(x: &'a [&i32]) -> impl Iterator<Item = ()> + 'a {
    x.iter().map(|_| ())
}

fn foo_mut<'a>(x: &'a mut [&i32]) -> impl Iterator<Item = ()> + 'a {
    x.iter().map(|_| ())
}

fn bar(x: &[&i32]) {
    for _ in foo(x) {}
}

fn bar_mut(x: &mut [&i32]) {
    for _ in foo_mut(x) {}
}

As shown you only need the outer lifetime because that's what your iterator type relies on. It technically also relies on the lifetime of &i32 but because nested references lifetimes are inherently constrained, you get that for free. You do still need the 'a because you need to tell the compiler what your impl Iterator is allowed to reference.

The key is that you should use different lifetimes whenever possible and elide when being explicit is unnecessary. Full code on the playground.


As a final note: because &'a mut [&'a i32] is overconstraining, you would later encounter issues using bar_mut. For example, you wouldn't be able to call it twice on the same value. See that failure on the playground. See Why does this mutable borrow live beyond its scope? that explains a bit more in a different scenario.

kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • Thank you! This was actually really helpful. I intentionally simplified my example but I don't think I would be able to avoid having nested lifetimes because I am storing a mutable reference in a struct. It looks like it might be easier to use a Rc> to accomplish what I wanted to do. – Drew Smith Aug 12 '23 at 17:13