Understanding (Static) Lifetimes

Hi there,

I am still new to Rust and while I think I understand lifetimes, I still struggle on occasions. Like this one. I am trying to do something seemingly simple here. I got it to work using the compiler errors (which are very good) as a guide, but I would like to understand what I have actually done here. I would much appreciate if someone could talk me though this. So here is the code in question:

pub struct EntityImporter<'a, 'b> {
    repositories: &'a Vec<Repository>,
    entity_pool: &'b EntityPool,
}

impl EntityImporter {
    pub fn new(repos: &Vec<Repository>, pool: &EntityPool) -> EntityImporter {
        EntityImporter {
            repositories: repos,
            entity_pool: pool,
        }
    }
}

The compiler tells me explicit lifetime required in the type of repos; lifetime 'static required and explicit lifetime required in the type of pool; lifetime 'static required. Interesting. I change the code to

impl EntityImporter {
    pub fn new(repos: &'static Vec<Repository>, pool: &'static EntityPool) -> EntityImporter {
        EntityImporter {
            repositories: repos,
            entity_pool: pool,
        }
    }
}

...and the error goes away.

Q1: Why is a static lifetime required here? What do the 'static lifetimes mean here? I understand they don't mean "this reference will live as long as the program" (there are various articles explaining that to be a misconception), but I don't understand what it actually means. I make the suggested change.

pub fn new(repos: &'static Vec<Repository>, pool: &'static EntityPool) -> EntityImporter {...}

Now, I am still left with missing lifetime specifiers; expected 2 lifetime parameters; help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from repos or pool.

Q2: I guess, that makes sense. I am thinking I need to tie the lifetime of the resulting EntityImporter to the two parameters. So, I change them to also be static, do I? Is that the right thing to do? Or should I have gone for some non-static lifetimes, e.g. 'x and 'y? (See the PS down the bottom).

pub fn new(repos: &'static Vec<Repository>, pool: &'static EntityPool) -> EntityImporter<'static, 'static> {...}

That leaves me with implicit elided lifetime not allowed here; help: indicate the anonymous lifetimes: <'_, '_> on the impl EntityImporter... line. I follow the suggestion and all is working.

Q3: However, why is the compiler happy with anonymous lifetimes here? Why do I not have to make them static? Or explicit (see below).

PS: This also works with non-static lifetimes. As I said, I still don't understand what the 'static lifetimes actually mean, so I am having a hard time decided what is right here (I used 'x and 'y here just to make it clear that they are nothing to do with the lifetimes in the struct itself.)

impl<'x, 'y> EntityImporter<'_, '_> {
    pub fn new(repos: &'x Vec<Repository>, pool: &'y EntityPool) -> EntityImporter<'x, 'y> {
        EntityImporter {
            repositories: repos,
            entity_pool: pool,
        }
    }
}

The most important thing here is that you don't want impl EntityImporter { here. It's unfortunate that the error message suggests "indicate the anonymous lifetimes: <'_, '_>" -- that's a good suggestion for function parameter types, but questionable for an inherent impl block.

The thing you're looking for to get past all these errors is this:

impl<'a, 'b> EntityImporter<'a, 'b> {
    pub fn new(repos: &'a Vec<Repository>, pool: &'b EntityPool) -> EntityImporter<'a, 'b> {
        EntityImporter {
            repositories: repos,
            entity_pool: pool,
        }
    }
}

Because this is basically a -> Self method, and you need to tie the lifetimes of the parameters to the lifetimes that are put on the struct, since you're putting those parameters into the struct.


Now, that said, you almost certainly don't want references inside your struct here. References in structs are not "I am trying to do something seemingly simple here" -- they're "the evil-hardmode of Rust".

Just keep owned values in your struct, like

pub struct EntityImporter {
    repositories: Vec<Repository>,
    entity_pool: EntityPool,
}

impl EntityImporter {
    pub fn new(repos: Vec<Repository>, pool: EntityPool) -> EntityImporter {
        EntityImporter {
            repositories: repos,
            entity_pool: pool,
        }
    }
}

(And if you do want references, you still don't want &Vec<T>, because &[T] lets you do all the same things but is more general.)

5 Likes

As @scottmcm pointed out, the suggestion of the compiler was a poor one. One question it didn't give you enough information to ask is, "why is lifetime elision not allowed here?" And the answer is, you have two non-&self "input lifetimes" and two "output lifetimes", and the compiler refuses to guess which lifetimes maps to what (or to unify the input lifetimes). More on lifetime elision from the nomicon.

4 Likes

Thank you, both.