Cleared Vec still holding onto borrow

The following code won't compile as I'm assigning to s which is borrowed by v. I understand that this would invalidate the reference, however because I've drained v before the assignment, there should be no references left to invalidate. Is there something I'm missing here?

fn main() {
    let mut v: Vec<&str> = Vec::new();
    let mut s = String::from("Foo");
    
    v.push(&s);
    v.drain(..).for_each(|s| println!("{s}"));
    
    s = String::from("Bar");
    v.push(&s);
}

I can workaround this by just re-allocating the vec after iteration, but I'd rather avoid that cost. Any suggestions?

If it helps, this would be for a game where references to textures and strings get stored in a Vec, which is iterated through at the end of the frame then cleared for the next. I'm still fairly new here, so any help would be appreciated!

The compiler doesn't know that drain removes all elements. You can use let s = ..., but I don't know if that would solve your real world issue (I assume this is a reduced version).

1 Like

It's possible you'll have other problems by storing references to strings, rather than owned strings, since all strings you reference will have to kept alive in the scope where the Vec is used.

1 Like

If it helps, lifetime analysis has no insight into what any method does. It's based entirely on methods' signatures, and the constraints there. It also happens entirely at compile time. As a result, draining or not draining the Vec cannot influence whether this code does or does not pass borrow checking.

It must be the case that references stored in v outlive v itself, or you could write this:

let mut v = Vec::new();
let s = String::from("foo");
v.push(&s);
drop(s); // oops! 

This would, if valid, allow you to construct a vec containing a reference to a dropped value, which is immediately undefined behaviour, and this would happen silently (that is, without any other warnings, until you run the program and it does something you don't expect - assuming it does do something you don't expect). That's obviously pretty bad, and Rust takes these kinds of soundness bugs seriously.

That constraint means that the elements of v must be references whose referrents have some lifetime longer than v, which in turn leads to the error you're seeing. Since the referrents must be borrowed for at least as long as v is alive, and since s is borrowed in that exact way, you can't drop or move s while v is alive.

Personally speaking, I very much doubt you want a vector of references. Vec<String> is going to be much more ergonomic, and you can borrow out of that vector much more easily than you can construct a usable vector of references borrowed from other places. However, if you do want a vector of references, you will need them to refer to something, which will not change while that vector exists (except possibly through that vector, if it holds mut refs, or through interior mutability).

2 Likes

Borrow checking is done at compile time, but whether there are any actual references around is a runtime property ... if the compiler even understood what Vec does on that level, which it does not.

But let me try to explain why the String must still be borrowed from another perspective.


let mut v: Vec<& /* 's */ str> = Vec::new();

The type of v is Vec<&'s str> for one specific lifetime, 's, that the compiler is going to try to infer. Every &str we push into the Vec has to have that same lifetime so that the types match.[1]

That means every borrow we push into v has to be valid for at least 's.

If it helps, think of it as a Vec<T> where T = &'s str. Naturally, you can only push Ts into a Vec<T>.

With that in mind...

    // 's has to be alive wherever we use it
    v.push(&s);  // ------------------------------+ we start borrowing
    // 's has to be alive                         | `s` up here and it
    v.drain(..).for_each(|s| println!("{s}")); // | has to be borrowed
    //                                            | for `'s`, which
    s = String::from("Bar"); // X                 | has to live until
    // 's has to be alive                         | at least here --+
    v.push(&s); // -------------------------------+ <---------------'

The borrow of s on the first push has to still be active at the second push, because otherwise we would be pushing references with two different lifetimes into the Vec -- that is, we would be trying to push values with two different types into v. We'd be trying to push a U into our Vec<T>.

So even if the compiler could understand that the Vec is empty, the types involved require that s is borrowed from the first push all the way to the second push, so that we are pushing Ts into our Vec<T>. That means it's borrowed on the line I've marked X -- where you overwrite s. But overwriting something while it is borrowed is an error.

If you want to store borrows of different durations, you need a new Vec, because a Vec<&'_ str> can only store borrows of a single duration.


Here's a good YouTube video that looks at a related lifetime error from the perspective of types.



  1. You can pass in something with a longer lifetime and it will coerce to the shorter lifetime, but you can't go in the other direction. ↩︎

3 Likes

There is a trick you can use to reuse a Vec allocation:

let mut v: Vec<&str> =
    v.into_iter().map(|_| unreachable!()).collect();

collect() of a Vec::into_iter() iterator is internally specialized to reuse the original Vec allocation, when the output element type has the same size and alignment. (This isn't guaranteed, but there's no reason to expect this optimization will go away.) In this case, since the Vec is empty, the map() function is called zero times, so it doesn't need to do any actual conversion of values.

Since you want to do this in a loop, you will need to do it twice: once at the end of the iteration to remove the lifetime (converting to Vec<&'static str> or Vec<*const str>) and once at the beginning of the next iteration. The Vec you carry from one iteration to the next must not have a non-'static lifetime (unless everything you borrow outlives the entire loop).

7 Likes

You can go from Vec<&'static str> to Vec<&'a str> with a simple variance coercion, so you won’t need to do it twice actually. (It probably still requires some manual let v = v;-style code though in order to actually allow that coercion.)


Edit: Apparently a single re-assignment like v = v.into_iter()…collect; without any let at the end of the loop just does the trick.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.