Why is an explicit lifetime needed in this Rust function output?

Viewed 130

I have a Config struct to store the global config. The config needs to be passed to different structs (abstract layers). For config consistency and saving memory, structs store a reference to the global config instead of a copy.

Then I have the implementation like the following: Playground

/// Global Config
#[derive(Debug)]
struct Config {
    pub version: String,
}

/// Layer A
#[derive(Debug)]
struct A<'cfg> {
    id: u32,
    config: &'cfg Config,
}

/// Layer B
#[derive(Debug)]
struct B<'cfg> {
    a_id: u32,
    b_id: u32,
    config: &'cfg Config,
}

impl<'cfg> A<'cfg> {
    pub fn new(id: u32, config: &Config) -> A {
        A { id, config }
    }
    
    #[allow(unused)]
    pub fn version(&self) {
        println!("{}_a{}", self.config.version, self.id);
    }

    pub fn create_many_b(&self) -> Vec<B<'cfg>> { // <-- Removing explicit `'cfg` lifetime for B here makes complier complain
        let cfg = self.config;
        let mut res = Vec::new();
        for id in 0..10u32 {
            res.push(B { a_id: self.id, b_id: id, config: cfg });
        }
        res
    }
}

impl<'cfg> B<'cfg> {
    pub fn version(&self) {
        println!("{}_a{}+b{}", self.config.version, self.a_id, self.b_id);
    }
}

fn create_many_a(config: &Config) -> Vec<A> {
    let mut res = Vec::new();
    for id in 0..5u32 {
        res.push(A::new(id, config));
    }
    res
}

fn main() {
    // Create a global config
    let cfg = Config { version: "1.0".to_owned() };
    println!("cfg pointer: {:p}", &cfg);
    // Use the global config to create many `A`s
    let a_vec = create_many_a(&cfg);

    let mut b_vec = Vec::new();
    // then create many `B`s
    for a in a_vec.into_iter() {
        b_vec.extend(a.create_many_b())
    }
    for b in b_vec.iter() {
        b.version();
    }
}

As my code comment said, the compiler rejects building when removing the lifetime mark (playground). My question is why it is required to have an explicit lifetime mark for B in the function create_many_b. How is the lifetime elision work in this example? Shouldn't the Bs created by an A via function create_many_b have the same lifetime as the A, which already has a 'cfg lifetime from the Config?

3 Answers

With these formulations

pub fn create_many_b(&self) -> Vec<B>
fn create_many_a(config: &Config) -> Vec<A>

the compiler deduces that the lifetime information missing for the result is identical to the lifetime of the reference given as argument. In the case of create_many_a() this is correct because the missing lifetime is exactly the lifetime of config (it's what we want). On the other hand, in the case of create_many_b() we don't want the Bs to contain a reference with a lifetime which is the lifetime of self (a A) but we want the lifetime used by the config member inside this A.

Thus, writing

impl<'cfg> A<'cfg> {
   ...
pub fn create_many_b(&self) -> Vec<B<'cfg>>

clearly states that the lifetime information used for B is the same as the one used for A. The A referenced by self can disappear, the reference inside the B is still valid because it does not depend on the A itself but on the Config the A was dependant on.

In the main() function

for a in a_vec.into_iter() {

introduces a A that will disappear at each iteration (they are consumed from a_vec), so the created Bs cannot last longer than this iteration if they depend on a, but they can if they depend on cfg which will still exist after this loop.

If you had written

for a in a_vec.iter() {

(still with the missing <'cfg> annotation) it would have been accepted by the compiler, but it would have been misleading since the Bs inside b_vec depend on the As inside a_vec and not directly on cfg which is what was intended in the definition of struct B<'cfg> {....

With elided lifetimes, the compiler will infer the lifetimes to be:

pub fn create_many_b<'a>(&'a self) -> Vec<B<'a>>;

This means that the Bs in the vector may not outlive the caller's reference to self.

If you were borrowing from data that was owned by self, this would be the correct lifetime: the data would be invalid after self was dropped. But the data is borrowed from config which is not owned by A, so you can make it less restrictive by expressing in the lifetimes where the data actually comes from:

pub fn create_many_b(&self) -> Vec<B<'cfg>>;

Often you wouldn't notice the difference in these two signatures, but your usage of these types actually requires the more permissive lifetimes. In particular, a_vec.into_iter() consumes a. This conflicts with the inferred lifetime because you then use the Bs in the vector after a is consumed.

Using the less restrictive lifetime allows you to use the Bs as long as config has not been dropped, regardless of what happened to the A.

When you remove the lifetime annotation, the compiler will infer it by filling the gaps. Something like this:

pub fn create_many_b<'a>(&'a self) -> Vec<B<'a>> {

The compiler is assuming that each B will live as long as the reference to self.

When you call this function:

for a in a_vec.into_iter() {
  b_vec.extend(a.create_many_b())
} <- "a" is dropped here

You are supposed to have a Vector of Bs, of which each one is holding a reference to a, which has already been dropped at this point.

So, basically, by removing the lifetime annotation you are telling the compiler to infer it, in which case the compiler will fill the lifetimes based on the reference it holds within the function, rather than the lifetime of A.

I hope this helps to understand the error message that you are receiving.

Related