Reuse a `Vec<T>'s` allocation beyond the scope of its content

Hi !

let mut v: Vec<&str> = Vec::new();

loop {
    let data = String::from("Hello, world!");

    // Fill the vector with some data bound to the scope of the loop.
    v.push(data.as_str());

    // Do things with the data.
    for s in &v { /* ... */ }

    // Clear the vector, ensuring there is no references left in the vector.
    v.clear();
}

This obviously does not compile because there is lots of ways for those references to leak outside of the loop. The main one being something panicking, or v.clear() not being called.

However, it seems reasonable enough to want to reuse the allocation of v in subsequent iterations of the loop.

This topic basically cover this exact case, but don't really like the solution proposed there.

I think its easier to solve this problem by using something like a VecAlloc.

struct VecAlloc {
    data: NonNull<()>,
    capacity: usize,
    layout: Layout,
}

impl VecAlloc {
    pub fn from_vec<T>(v: Vec<T>) -> Self {
        let mut v = ManuallyDrop::new(v);
        v.clear();

        Self {
            data: NonNull::new(v.as_mut_ptr()).unwrap().cast(),
            capacity: v.capacity(),
            layout: Layout::new::<T>(),
        }
    }

    pub fn into_vec<T>(self) -> Vec<T> {
        assert_eq!(Layout::new::<T>(), self.layout);

        let this = ManuallyDrop::new(self);

        unsafe { Vec::from_raw_parts(this.data.as_ptr().cast(), 0, this.capacity) }
    }
}

impl Drop for VecAlloc { /* ... deallocate */ }
// `Option` ensures that if something goes wrong in the loop, there won't be a double-free.
let mut valloc: Option<VecAlloc> = Some(VecAlloc::from_vec(Vec::new())); 

loop {
     let v = valloc.take().unwrap().into_vec::<&str>();

    // Do things

    v = Some(VecAlloc::from_vec(v));
}

playground

Is there already a crate to do this? Because I'd rather use something well-tested. Otherwise, does this solution seem sound?

If I'm understanding your question correctly, yes, I think: Pattern: how to reuse a `Vec<&str>` across loop iterations? - #13 by mbrubeck

2 Likes

It might be of interest that it's possible to recycle a Vec without any unsafe code at all, by taking advantage of vec.into_iter().collect() reusing the backing storage. Here's a toy example of doing this to enable reusing storage for borrows with different lifetimes and different types too:

use std::mem::size_of;
use std::path::{Path, PathBuf};

struct ReusableBuffer<B> {
    vec: Option<Vec<B>>,
}

impl<B> ReusableBuffer<B> {
    pub fn new() -> Self {
        Self { vec: None }
    }

    pub fn take<T>(&mut self) -> Vec<T> {
        // Not for safety, only to ensure the expected performance characteristic.
        // Strictly we should check the alignment too.
        debug_assert!(size_of::<B>() == size_of::<T>());
        
        let mut vec = self.vec.take().unwrap_or_else(Vec::new);
        vec.clear();
        let converted: Vec<T> =
            vec.into_iter().map(|_| unreachable!()).collect();
        converted
    }
    
    pub fn put<T>(&mut self, mut returned: Vec<T>) {
        debug_assert!(size_of::<B>() == size_of::<T>());

        returned.clear();
        self.vec = Some(returned.into_iter().map(|_| unreachable!()).collect());
    }
}

fn main() {
    let mut b = ReusableBuffer::<&str>::new();
    
    {
        let s = String::from("foo");
        let mut v: Vec<&str> = b.take();
        v.push(&*s);
        println!("{:p}, {:?}", &*v, v);
        b.put(v);
    }
    {
        let p = PathBuf::from("/bar");
        let mut v: Vec<&Path> = b.take();
        v.push(&*p);
        println!("{:p}, {:?}", &*v, v);
        b.put(v);
    }
}

This doesn't have any particular advantage over the crates linked above, other than relying solely on std's unsafe rather than additional unsafe.

6 Likes

An ad-hoc implementation of the approach @kpreid mentioned to your example code looks like this

let mut empty_v: Vec<&str> = Vec::new();

loop {
    let mut v = empty_v;

    let data = String::from("Hello, world!");

    // Fill the vector with some data bound to the scope of the loop.
    v.push(data.as_str());

    // sanity check: address stays the same
    println!("{:p}", &v[..]);

    // Do things with the data.
    for s in &v { /* ... */ }

    // Clear the vector, ensuring there is no references left in the vector.
    v.clear();
    empty_v = v.into_iter().map(|_| unreachable!()).collect();
}

(adapted from @kpreid's solution above as well as the code in a previous post of mine here)

Note that (as explained in the other post and subsequent discussion) you can skip the into_iter+collect step on the take side due to variance.

3 Likes