Please smash this lifetime evil

// Definitely the lifetime given to v2 is the trouble maker.
// But it's needed in my program.
fn toy_func <'a> (v1:&mut Vec<&'a u8>, v2:&'a mut Vec<u8>){

}

fn main(){
    let mut v1:Vec<&u8>=Vec::new();
    let mut v2:Vec<u8>=Vec::new();
    for _ in 0..2 {
        // ****** Hovering over on "&mut v2" shows this:
        // &mut Vec<u8, Global>
        // cannot borrow `v2` as mutable more than once at a time
        // `v2` was mutably borrowed here in the previous iteration of the looprustcE0499
        // ***** This lifetime evil is just gone if no looping
        toy_func(&mut v1, &mut v2);
    }
}

This comes from my previous post where I try to share references on String in favor of speed, eventually [usize;20] becomes the winner, but at the price of cloning of [usize;20]. This lifetime evil shows up when I turn to try lifetime stuff.

I think we're gonna need more context. Probably you're going to have to change the lifetimes of toy_func.

"A trouble maker" is not the lifetime given to v2, but the fact that it is equal to lifetime inside v1.

Currently, you're saying the following: "this function accepts a vector of references, all of which survive inside some region, and an exclusive reference to another vector, which must be alive for the same region". That's not the semantics you really have, however: there are two lifetimes in signature of toy_func, not one.

4 Likes

This is the minimal version of the problem in terms of code size, people don't like big block of codes, so I just model the problem into least codes. Anyway, I'll try to prepare the necessary context, but, what could be the reasonable explaination on this simple codes per se?

To give some idea why Rust doesn't allow you to call a function with such lifetime annotations within a loop, consider the following example implementation:

fn toy_func <'a> (v1:&mut Vec<&'a u8>, v2:&'a mut Vec<u8>){
    v2.push(1);
    v1.push(&v2[v1.len()-1])
}

This adds an element to v2 and then adds a reference to this element in v1. However, on the next call, v2 gets further extended, possibly reallocating and invalidating the references in v1. Either you need to decouple the 'a lifetimes to make it clear that you are not doing anything like this, or make v2 a & reference so that you can share references to it, or something more complex depending on your actual requirements.

3 Likes

Here's the "minimal" fix:

-fn toy_func <'a> (v1:&mut Vec<&'a u8>, v2:&'a mut Vec<u8>){
+fn toy_func(v1: &mut Vec<&u8>, v2: &mut Vec<u8>) {

This makes the example work. If this makes other stuff not work, then that other stuff also needs to be part of the minimal example, because we can't solve the problem without it.

6 Likes

Here is the minimal version with context:

#[derive(Clone, Debug)]
struct State {
    pub id:[usize;20],
}

impl State{
    pub fn id(&mut self) -> [usize;20]{
        [91;20]
    }
}

fn place_pieces() -> Vec<State>{
    let mut states:Vec<State>=Vec::new();
    let mut dedup_ids:Vec<&[usize;20]>=Vec::with_capacity(93000);
    let state=State{
        id:[91;20]
    };
    states.push(state);
    return place_pieces_recursively(states, &mut dedup_ids);
}

fn place_pieces_recursively <'b> (states:Vec<State>, dedup_ids:&mut Vec<&'b [usize;20]>) -> Vec<State>{
    let mut temp_states:Vec<State>=Vec::new();
    for state in states.iter() {
        // Here is the same problem:
        // cannot borrow `temp_states` as mutable more than once at a time
        // `temp_states` was mutably borrowed here in the previous iteration of the loop
        place_a_piece(dedup_ids, &mut temp_states, state);
    }

    // As a consequence, it results in this problem too:
    // cannot move out of `temp_states` because it is borrowed
    // move out of `temp_states` occurs here
    return place_pieces_recursively(temp_states, dedup_ids);
}

fn place_a_piece<'a, 'b:'a>(dedup_ids:&mut Vec<&'a [usize;20]>, states:&'b mut Vec<State>, state:&State){
    let mut spawned_state=state.clone();
    let new_id=spawned_state.id();
    spawned_state.id=new_id;
    states.push(spawned_state);
    // The whole efforts I make is to have dedup_ids holds references to State.id, that's all.
    // It turns out it's so complicted using lifetime
    dedup_ids.push(&states.last().unwrap().id);
}

fn main(){
    place_pieces();
}

You are right, if put 'b on v2, no errors anymore, I don't understand though, but in my context it doesn't work, please see below post with context.

Your code is trying to do exactly what @jameseb7 showed you that could go wrong, meaning rust lifetimes just saved you from a bug.

I think you have 2 possible solutions:

  • Directly store [usize; 20] inside dedup_ids without borrowing, this will be a bit slower since it will copy/move more bytes, but it should be an easier solution. (ps: have you considered using a smaller type for the ids?)
  • In dedup_ids store index into states. This will be fast and you can get the id when you want by doing &states[index].id(), however this is more error prone

ps: I guess you're doing this to learn the language, but isn't this too complex as a learning project? I think you should try something easier to get a grasp of the basics before moving to more intermediate stuff.

pps: place_pieces_recursively doesn't have an exit condition, so it will recurse infinitely. You probably want to change that.

2 Likes

It's been done already, I just try to speed up using lifetime instead of cloning. Smaller type is easy to think of and change, that's ignored temporarily.

I am not sure if this is going to work, myabe I didn't get it.

Yeah, I'm not experienced in Rust, I think both reading and making something real are important. place_pieces_recursively does have a base case for stopping recursion, I just removed all irrelevant stuff for the sake of simplicity to focus on the problem alone.

Make sense, if so, Rust compiler is really thoughtful even with an empty function body. Thank you.

It's true, rust is! You can stub out function signatures and just put unimplemented!() in the body and rust will use the signatures when compiling to check that lifetimes and types line up correctly.

Can be a handy trick to scaffold more complex code and fill in details later once the big picture works.
The macro will just panic. I think the types work out because of the ! (Never) type "returned" by panics :slight_smile:

1 Like