Lifetime in method params, data flow and impl Trait (or how I lost my mind)


#1

Consider a method that accepts a lifetime not related to struct:

struct Mother<'p> {
    kids: Vec<&'p str>,
}

impl<'p> Mother<'p> {
    // find kids with similar names defined in `names`
    fn search<'n, I, S>(&'p self, names: &'n I) -> Vec<&'p str>
    where
        &'n I: IntoIterator<Item = S>,
        S: AsRef<str>,
    { // `I` is either a vector of `String` or slice of `str`
        self.kids
            .iter()
            .filter( /* use I here */ )
            .collect()
    }
}

Here lifetime 'n is defined by caller and search is enforcing that returned references in Vec match those of &self's lifetime (both have 'p)

My line of thinking is that whenever I call search, I shouldn’t be (manually) concerned about 'n since the compiler will yell at me if somehow names is not in scope and dead. And I think this compiling snippet validates (kinda) my thought.

As you can see &names only lives in runner function and we are returning a Vec of references. To come up with those references we have utilized &names but we are not actually returning them:

fn runner<'p>(mother: &'p Mother) -> Vec<&'p str> {
    let names = vec!["kid1", "kid2"];
    find_kids(&names, mother)  // `names` still in scope while we use it
} // `names` is dropped here, but we are still able to return references from
  // this function. seems we don't need `names` anymore!

fn find_kids<'n, 'p, I, S>(names: &'n I, mother: &'p Mother) -> Vec<&'p str>
where
    &'n I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    mother.search(names)
}

Q1: So can we essentially say that search “doesn’t care” about the lifetime 'n? even though it uses it to filter stuff?

Which brings me to my non-compiling version of the code. If instead of returning Vec<&'p str>, we return impl Iterator<Item = &'p str> then it seems the lifetime 'n does matter as the compiler is not happy about early death of names in runner function.

fn runner<'p>(mother: &'p Mother) -> impl Iterator<Item = &'p str>
{
    let names = vec!["kid1", "kid2"];
    find_kids(&names, mother)
}  // oops, seems `names` is still needed!!! what gives?

fn find_kids<'n, 'p, I, S>(names: &'n I, mother: &'p Mother) -> impl Iterator<Item = &'p str>
where
    &'n I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    mother.search(names).into_iter()
}

Q2: What makes impl Trait think it’s better than a Vec of references that it should demand stricter lifetime spans?


#2

It’s still needed because you’re iterating from the reference &Vec. Nothing in the return value owns the vector being referenced, and it be dropped when it goes out of scope.

Sorry, I missed the fact that you collect to a new Vec, and then into_iter() that. And indeed, it works if find_kids is explicit about the return type -> std::vec::IntoIter<&'p str>


#3

I think the issue here is somewhat similar to how closures are desugared - the impl Iterator synthetic struct is capturing that local vec, but it’s dead after the function. It’s true that the vec isn’t needed but the compiler can’t see this across the function call signatures. Maybe this is a bug or a limitation in how lifetimes in scope are deduced.


#4

I think you may have a legitimate bug here – somehow impl Trait seems to be forcing 'n: 'p here, which is not really necessary, nor desired.

I had filed a sort of similar problem in rust#50823, but your case is not solved by NLL nor the same assignment workarounds that I was able to use there.


#5

If the desugaring involves 2 distinct structs, one for runner and another for find_kids, then runner needs the vec to eventually call find_kids. In that case, it’s not a bug.


#6

I don’t think the closures matter at all – you can remove Mother::search and use a simplified find_kids, and the problem remains:

fn find_kids<'n, 'p, I, S>(_: &'n I, _: &'p Mother) -> impl Iterator<Item = &'p str>
where
    &'n I: IntoIterator<Item = S>,
{
    vec![].into_iter()
}

But strangely, if you also remove S, it compiles!

fn find_kids<'n, 'p, I>(_: &'n I, _: &'p Mother) -> impl Iterator<Item = &'p str>
where
    &'n I: IntoIterator,
{
    vec![].into_iter()
}

#7

I’m on mobile and can’t really play with this at the moment but essentially we want the functions to say (I think) that they return impl Iterator<Item = &'p str> + 'static. I’m assuming that doesn’t work as well but maybe sheds a bit more light on what the compiler is thinking? Although maybe that formulation is non-sensical here; we want to say that the impl doesn’t capture anything beyond what &p is, and I don’t know offhand if you can say that.


#8

Btw, anyone know precisely where lifetime captures are described for impl Trait in return position? The RFC is hard to follow with a lot of noise and impl Trait in arg position conversations.


#9

I filed: https://github.com/rust-lang/rust/issues/51069


#10

Interestingly, this formulation works. It does change the bounds such that the IntoIterator returns &'n S, however. So it seems like the &'p str that’s the S in the original code causes 'p to be captured. Maybe.

Returning a Box<Iterator<Item = &'p str> + 'p> in find_kids is also enough to let the rest compile as-is.


#11

thanks for your clarifications.
I also found another way of making it work, not sure if it’s helpful.


#12

Drop the S and using <&'n I as IntoIterator>::Item: AsRef<str> compiles.