How does Rust know that the lifetime of a vector is dependent on things you push to it?

Viewed 186

I have a vector of references, and push a reference to a value, move the value and then attempt to use the vector:

fn main() {
    let mut my_vec: Vec<&Box<i32>> = vec![];
    let my_val = Box::new(5);
    my_vec.push(&my_val);
    drop(my_val);
    println!("{:?}", my_vec)
}

This gives us an error:

error[E0505]: cannot move out of `my_val` because it is borrowed
 --> src/main.rs:5:10
  |
4 |     my_vec.push(&my_val);
  |                 ------- borrow of `my_val` occurs here
5 |     drop(my_val);
  |          ^^^^^^ move out of `my_val` occurs here
6 |     println!("{:?}", my_vec)
  |                      ------ borrow later used here

If this was allowed to compile, we would get a use after free so it's good that Rust is able to detect that the vector can't outlive things it references.

If we look at this similar piece of code

fn dont_push<T>(_v: &mut Vec<T>, _a: T) {}

fn main() {
    let mut my_vec: Vec<&Box<i32>> = vec![];
    let my_val = Box::new(5);
    dont_push(&mut my_vec, &my_val);
    drop(my_val);
    println!("{:?}", my_vec)
}

We get the same error:

error[E0505]: cannot move out of `my_val` because it is borrowed
 --> src/main.rs:7:10
  |
6 |     dont_push(&mut my_vec, &my_val);
  |                            ------- borrow of `my_val` occurs here
7 |     drop(my_val);
  |          ^^^^^^ move out of `my_val` occurs here
8 |     println!("{:?}", my_vec)
  |                      ------ borrow later used here

This seems to suggest that Rust looks at the type signature of push and dont_push to figure out that it's possible that my_vec requires my_val to still be alive.

What is the general rule?

From my experimenting here, it looks like it might be something along the lines of "if one argument to a function is a mutable reference, that argument can't outlive the other ones". What happens if there are multiple mutable references?

1 Answers

This seems to suggest that Rust looks at the type signature of push and dont_push to figure out that it's possible that my_vec requires my_val to still be alive.

What is the general rule?

The type variable T in this particular call to dont_push becomes &'a Box<i32>. It's a reference, so it has some lifetime 'a. But there are two incompatible choices for what that lifetime is, deriving from the two parameters:

  • A Vec<T> must not end up holding Ts that are invalid, so we conclude that T's lifetime(s) must outlive the Vec. Thus, the lifetime must extend until my_vec is no longer used. (This is true of almost any generic type, not just Vec — except for “contravariance”, which you will rarely encounter.)
  • The second parameter uses the same type variable for a borrow of my_val. Thus, the lifetime must end before my_val is dropped.

We can produce the same error with this simpler program:

fn main() {
    let my_short: Box<i32> = Box::new(123);
    let my_long = &my_short;

    drop(my_short);
    println!("{:?}", my_long);
}

In this case, the requirement for the lifetimes to match is played by the assignment that initializes my_long. But just like dont_push doesn't need to do anything, we don't need the effect of the assignment; we just need something requiring the types to match.

There are similar situations where you won't get this error. For example, if my_val implements Copy (e.g. because it is i32 rather than Box<i32>) then drop(my_val) drops a copy and the original is still valid. The Copy value has to be inside a non-Copy container so that the drop necessarily deallocates the memory:

fn main() {
    let my_short: Vec<i32> = vec![123];
    let my_long = &my_short[0];

    drop(my_short);
    println!("{:?}", my_long);
}
Related