How is the "outlives trick" wrong?

I have the following function which seems to be working for all the use cases I currently have.

pub fn filter_strs<'a, 'b: 'a>(
    str_iter: impl IntoIterator<Item = &'a str> + 'b,
    search_term: &'b str,
) -> impl Iterator<Item = String> + 'a + 'b {
    str_iter
        .into_iter()
        .filter_map(move |s| s.contains(search_term).then_some(s.to_owned()))
}

I read the RFC for the new capture rules in the 2024 edition, and it says I’m using is “outlives trick”.

“But this is actually a non-solution in the general case.”

I see that I should be writing the signature like this instead:

pub fn filter_strs<'a, 'b, I>(
    str_iter: I,
    search_term: &'b str,
) -> impl Iterator<Item = String> + use<'a, 'b, I>
where
    I: IntoIterator<Item = &'a str>;

However, I don’t actually understand the difference. The explanation in the RFC is too abstract for me to understand. I tried writing a concrete program that fails where it shouldn’t, playground link. However, all the examples I wrote still pass the borrow checker. Can someone show me an actual example where this fails and the new, more correct use<> syntax passes?

The meaning of + 'long + 'short is the same as + 'long and means that the type can't use lifetime annotations shorter than 'long. I.e. it can't use 'short anywhere.

The meaning of + use<'long, 'short> means that the type can only use the lifetime annotations 'long and 'short. I.e. it can use 'short.

For your case it means that the second lifetime is redundant. Your code might as well be

pub fn filter_strs<'a>(
    str_iter: impl IntoIterator<Item = &'a str> + 'a,
    search_term: &'a str,
) -> impl Iterator<Item = String> + 'a {
    str_iter
        .into_iter()
        .filter_map(move |s| s.contains(search_term).then_some(s.to_owned()))
}

This means that you can't have search_term be with a longer lifetime than the one on str_iter. But since you can't get search_term out of the iterator as a &str, that doesn't really matter anyway.

4 Likes
fn fails<'a>(i: impl IntoIterator<Item = &'a str> + 'a) {
    let search_term = "error".to_owned();
    let err_lines = filter_strs(i, &search_term);
    eprintln!("{:#?}", err_lines.collect::<Vec<_>>());
}
error[E0597]: `search_term` does not live long enough
   --> src/main.rs:103:36
    |
101 | fn fails<'a>(i: impl IntoIterator<Item = &'a str> + 'a) {
    |          -- lifetime `'a` defined here
102 |     let search_term = "error".to_owned();
    |         ----------- binding `search_term` declared here
103 |     let err_lines = filter_strs(i, &search_term);
    |                     ---------------^^^^^^^^^^^^-
    |                     |              |
    |                     |              borrowed value does not live long enough
    |                     argument requires that `search_term` is borrowed for `'a`
104 |     eprintln!("{:#?}", err_lines.collect::<Vec<_>>());
105 | }
    | - `search_term` dropped here while still borrowed
3 Likes

Thank you! This was the example I was looking for. I thought it would instantly click when I saw this… but now I’m confused why this function compiles?

fn foo1() {
    let lines = vec![
        "info: whatever whatever",
        "debug: nobody cares",
        "error: everything sucks",
    ];
    {
        let err_search = ERR_SEARCH.to_owned();
        let search_term = &err_search;
        {
            let err_lines = filter_strs(lines, search_term);
            eprintln!("{:#?}", err_lines.collect::<Vec<_>>());
        }
        println!("{search_term}");
    }
}

This seems like the same thing to me. Why does it fail when there is a function boundary in between?

It's because the compiler is able to coerce your Vec<&'static str> into a Vec<&'b str> by making the lifetime shorter so that the lifetime fits. (Here you may think of 'b as the scope of search_term.)

With impl IntoIterator, the compiler is not able to shorten the lifetime inside the iterator because the underlying type may be one where that isn't possible. That is, the lifetime is invariant.

4 Likes

I see I see. Thank you for clarifying :person_bowing:

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.