Help with struct lifetimes

Hi all,
I'm quite new to Rust and, despite I've been reading thoughtfully the Rust Programming language and Programming in Rust books, I'm still struggling with lifetimes. This is a simplification of a code I'm currently working on:

pub struct G {
    pub name: String,
    pub p: &P,
}

impl G {
    pub fn new(name: String, p: &P) -> G {
        G {
            name: name,
            p: p,
        }
    }
}

pub struct S {
    pub gs: Vec<G>,
}

impl S {
    pub fn new() -> S {
        S {
            gs: Vec::new(),
        }
    }

    pub fn add_gs(&mut self, i: u32, p: &P) {
        let g = G::new(format!("g_{}", i));
        self.gs.push(g);
    }
}

pub struct P {
    pub big: Vec<i32>,
}

impl P {
    pub fn new() -> P {
        P {
            big: Vec::with_capacity(100000 as usize),
        }
    }
}

fn main() {
    let p = P::new();
    let mut s = S::new();
    for i in 1..5 {
        s.add_gs(i, &p);
    }
}

Basically, the idea is to save unnecessary copy()'s of a large chunk of data (big).

I'm asking for your help to understand which lifetime logic could make this code working properly (if there is any) as I've tried without success.

Thanks in advance for your help, time and patience,
Brian

Do you need to modify the thing after you start sharing it? Because if not, I recommend putting it in an Arc. This allows you to clone it very cheaply, as every clone of the arc is just a new handle to the same object.

1 Like

That is a very interesting workaround! I didn't know anything about Arc, thanks a lot for pointing me to it.

In case I would need to modify it or not using the Arc object, is there any possible pure lifetime solution?

Thanks a lot for your quick and useful answer!

Well you can do it like this. Things you should be aware of:

  1. While any object containing an immutable reference to p exists, you cannot modify p.
  2. Objects containing references to p must go out of scope before p goes out of scope.

Note that the immutability in 1 is the same as what makes Arc require immutability. The purpose of Arc is to avoid thing 2 above by making the object with the reference be able to be alive in any scope. It does this by having the Arc keep the object alive, which a reference doesn't do.

pub struct G<'a> {
    pub name: String,
    pub p: &'a P,
}

impl<'a> G<'a> {
    pub fn new(name: String, p: &'a P) -> Self {
        G {
            name: name,
            p: p,
        }
    }
}

pub struct S<'a> {
    pub gs: Vec<G<'a>>,
}

impl<'a> S<'a> {
    pub fn new() -> Self {
        S {
            gs: Vec::new(),
        }
    }

    pub fn add_gs(&mut self, i: u32, p: &'a P) {
        let g = G::new(format!("g_{}", i), p);
        self.gs.push(g);
    }
}

pub struct P {
    pub big: Vec<i32>,
}

impl P {
    pub fn new() -> P {
        P {
            big: Vec::with_capacity(100000 as usize),
        }
    }
}

fn main() {
    let p = P::new();
    let mut s = S::new();
    for i in 1..5 {
        s.add_gs(i, &p);
    }
}

playground

1 Like

If you need to modify objects that are shared (either through references or Arc or any other method for sharing things), you need to use some kind of cell. For simple types, there's Cell, and for more complicated types there is RefCell. For multi-threaded applications there is Mutex. There are also special kinds of integers in the atomics module that can be modified with only shared access using special CPU instructions.

Basically, a mutable reference &mut T is actually a requirement that you have exclusive access to the object. Things like RefCell allow exclusive access through shared access by ensuring that only one thing is accessing it at a time. It does this by panicking if several things try to obtain a &mut T.

1 Like

Thanks a lot again, superb explanation and different approaches I could try.

It's worth mentioning that <'a> on a struct means "This object contains a reference to something outside itself". In particular, you can't have references from one field to another.

I'd like to mention something about the impl block on S<'a>.

pub struct S<'a> {
    pub gs: Vec<G<'a>>,
}
impl<'a> S<'a> {
    pub fn new() -> Self {
        S {
            gs: Vec::new(),
        }
    }

    pub fn add_gs(&mut self, i: u32, p: &'a P) {
        let g = G::new(format!("g_{}", i), p);
        self.gs.push(g);
    }
}

Notice how the p argument in add_gs has an 'a on it? Notice that the vector in the definition has the same lifetime marker? This tells the compiler that you can't just pass add_gs any reference — the reference must live for long enough so you can put it in the vector. If you change it to just &P, you will get an error, because you haven't told the compiler it must live for long enough to be inside the vector:

error[E0621]: explicit lifetime required in the type of `p`
  --> src/main.rs:28:22
   |
26 |     pub fn add_gs(&mut self, i: u32, p: &P) {
   |                                         -- help: add explicit lifetime `'a` to the type of `p`: `&'a P`
27 |         let g = G::new(format!("g_{}", i), p);
28 |         self.gs.push(g);
   |                      ^ lifetime `'a` required

The error goes away if you don't try to put it inside the vector. For example, without the lifetime marker on the argument, this would compile:

fn main() {
    
    let mut s = S::new();
    for i in 1..5 {
        let shortlived_p = P::new();
        s.add_gs(i, &shortlived_p);
    }
    
    // if shortlived_p was in the vector now,
    // we could now print uninitialized memory,
    // as shortlived_p has been destroyed
    println!("{:?}", s);
}

playground

Note that the push to the vector has been commented out in the playground link above. If you make it a non-comment, you get this error: playground.

1 Like

Thanks again for the extra explanation, top!

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.