I can't compile this snippet with the nightly rust:
#![feature(iter_collect_into)]
use std::path::PathBuf;
pub fn common_path(paths: Vec<PathBuf>) {
let mut buf = Vec::new();
for path in paths {
path.components().collect_into(&mut buf);
buf.clear();
}
}
It fails with the error:
|
7 | for path in paths {
| ---- binding `path` declared here
8 | path.components().collect_into(&mut buf);
| ^^^^ -------- borrow later used here
| |
| borrowed value does not live long enough
9 | buf.clear();
10 | }
| - `path` dropped here while still borrowed
Am I hitting a limitation of the borrow checker here?
no, this is intended behavior of the borrow checker.
lifetime is associated with a region of the source code, it's not about runtime behavior. the buf defined outside the loop cannot contain borrowed values within the loop region.
There is nothing that can tell the borrow checker that the Vec will contain no elements, and hence no borrows, after the call to .clear().
Note that the borrow checker can reason about a variable no longer containing a borrow, but only if you reassign it with a value that does not hold that borrow.
Here's a way to fix your example Rust Playground (it exploits the buffer reuse when collecting an iterator to avoid reallocation, though this is not technically guaranteed to work; it can be replaced with the more reliable Vec::from_raw_parts at the cost of a small amount of unsafe).
The key point is that the reuse method doesn't just clear the Vec, but returns a different Vec with a possibly different type. The compiler is thus able to infer Component<'a> but for a different lifetime 'a, and thus allows the borrow checker to determine that the old borrow is no longer used in that Vec.
I see you are trying to reuse allocated memory, for Vec, you can put values borrowed for longer lifetime into Vec of shorter lifetime, not the other way around.
besides the solution by @SkiFire13 , an alternative is to use a arena-based allocator, which has very low overhead. for example, with the bumpalo crate, you can rewrite the example like this:
use bumpalo::{Bump, collections::Vec};
pub fn common_path(paths: Vec<PathBuf>) {
// Create a new bump arena.
let mut bump = Bump::new();
for path in paths {
// this `Vec` is allocated from the arena
let mut buf = Vec::new_in(&bump);
// I use `Extend::extend()`, which doesn't need nightly
// instead of `Iterator::collect_into()`
buf.extend(path.components());
// do the work
drop(buf);
bump.reset();
}
}
#![feature(iter_collect_into)]
use std::path::PathBuf;
pub fn common_path(paths: Vec<PathBuf>) {
let mut buf = Vec::new();
for path in paths {
path.components().collect_into(&mut buf);
buf = reset_vec(buf);
}
}
fn reset_vec<T, U>(mut vec: Vec<T>) -> Vec<U>
{
// This function will work even if the asserts would fail, but it will only reuse the allocation if they would succeed. Feel free to uncomment them if you prefer
// assert!((size_of::<T>() * vec.capacity()) % size_of::<U>() == 0);
// assert!(align_of::<T>() == align_of::<U>());
vec.clear();
vec.into_iter().map(|_| unreachable!()).collect()
}
In reset_vec:
into_iter() convinces the compiler we are consuming the items.
map() convinces the compiler the items are a different (owned) type, guaranteeing we aren't keeping the original items around (but of course we've cleared the Vec so there are 0 items).
collect() (as an optimization) reuses the existing allocation provided that the size and alignment of the item types are the same, and we know that they are the same, as they are actually the same type.