NLL does not seem to work on lifetime boundaries

Consider two examples:

Rust 2021

fn indices<'s, T>(
    slice: &'s [T],
) -> impl Iterator<Item = usize> + 's {
    0 .. slice.len()
}

Rust 2024link here

fn indices<'s, T>(
    slice: &'s [T],
) -> impl Iterator<Item = usize> {
    0 .. slice.len()
}

Both of the above examples will cause the following code to fail to compile

fn main() {
    let mut data = vec![1, 2, 3];
    let mut i = indices(&data);
    data.push(4); // <-- Error!
}

This code fails to compile and the error message is as follows:

error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let mut i = indices(&data);
  |                         ----- immutable borrow occurs here
4 |     data.push(4); // <-- Error!
  |     ^^^^^^^^^^^^ mutable borrow occurs here
5 | }
  | - immutable borrow might be used here, when `i` is dropped and runs the destructor for type `impl Iterator<Item = usize> + '_`

According to NLL rules, the lifetime of variable i should have ended, right?

Consider the following examples that can be compiled

  • 1

    fn main() {
        let mut data = vec![1, 2, 3];
        {
            let mut i = indices(&data);
        }
        data.push(4); // <-- Pass!
    }
    
  • 2

    fn main() {
        let mut data = vec![1, 2, 3];
        indices(&data);
        data.push(4); // <-- Pass!
    }
    

Does this mean that NLL does not work on lifetime boundaries?

You mean that it doesn't work on function boundaries? Yes, that is indeed the case. If that wasn't the case, changing the body of a function may cause accidental breaking changes for all callers.

Edit: Misread your code. What happens here is that i is kept alive until after the data.push(4) as rustc conservatively assumes that the type of i implements Drop, which would be called only once i gets out of scope rather immediately when i is no longer used. NLL only applies to variables whose type is known to not implement Drop. If you want your code to use NLL, you did have to replace the impl Iterator<Item = usize> return type of indices with Range<usize>, which is the actual type of 0..slice.len() and known to both not implement Drop and not borrow from the input slice. (either is enough to make your code work)

2 Likes

Thank you for your explanation! As you mentioned, the issue with NLL "failing" was indeed caused by the potential Drop trait "extending" the lifetime of the variable i. I realize this phenomenon might become more common in Rust 2024, as impl Trait now includes lifetimes by default. Thanks again for your help!

Edit: For now, another solution seems to be using 'static.

fn indices<'s, T>(
    slice: &'s [T],
) -> impl Iterator<Item = usize> + 'static { // <-- Pay attention here
    0 .. slice.len()
}

fn main() {
    let mut data = vec![1, 2, 3];
    let mut i = indices(&data);
    data.push(4); // <-- Pass!
}

Edit2: Using a separate lifetime also seems to work...

fn indices<'s, 'i, T>(
    slice: &'s [T],
) -> impl Iterator<Item = usize> + 'i {
    0 .. slice.len()
}

fn main() {
    let mut data = vec![1, 2, 3];
    let mut i = indices(&data);
    data.push(4); // <-- Pass!
}

It's not really about NLL or drop. A signature where the return type captures the input lifetime requires that the input lifetime be borrowed for that lifetime.

Consider this signature, where the return doesn't implement Drop and captures the lifetime even though it contains no references.[1] It's still important that the source slice remain borrowed while the iterator is alive.

Opaque types that capture lifetimes make it possible for the implementation to be or to change to something that requires the source to remain borrowed. If the function implementer wants to commit to not needing the borrow, they will be able to opt out of the lifetime capturing with use bounds.

fn indices<T>(
    slice: &[T],
) -> impl Iterator<Item = usize> + use<T> {
    0 .. slice.len()
}

  1. an implementation detail, but pointed out for further clarification ↩︎

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.