Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Performing a reverse split_off operation in Rust efficiently

Tags:

rust

My use case involves two mutable vectors a and b and a usize parameter x. I want to make the following change:

  1. take the elements b[0..x] and append them to the end of a (changing capacity of a as required)
  2. transform b into b[x..], without changing the original capacity of b

Currently I do the following:

while a.len() < x && !b.is_empty() {
    a.push(b.pop_front().unwrap());
    // here `b` is a VecDeque but I am happy to use a Vec if pop_front was not required
}

Obviously this seems a very slow operation, checking two conditions and calling unwrap at every iteration. It would be great if there was a rev_split_off operation such that:

let mut front = b.rev_split_off(x);
a.append(&mut front);

Here rev_split_off returns a newly allocated vector for the slice b[0..x] and transforms b into the remaining slice with unchanged capacity.

Question: How to perform my use case efficiently, with or without using such a thing as rev_split_off?

like image 470
Gaurang Tandon Avatar asked May 02 '26 11:05

Gaurang Tandon


1 Answers

Well, I think you will have to implement the rev_split_off yourself (even though I would probably call it split_off_back but it's the same).

Here is how I would implement it:

/// Moves the `i` first elements of `vec` at the end of `buffer`.
fn split_off_back<T>(vec: &mut Vec<T>, i: usize, buffer: &mut Vec<T>) {
    // We have to make sure vec has enough elements.
    // You could make the function unsafe and ask the caller to ensure
    // this condition.
    assert!(vec.len() >= i);

    // Reserve enough memory in the target buffer
    buffer.reserve(i);
    // Now we know `buffer.capacity() >= buffer.len() + i`.

    unsafe {
        // SAFETY:
        //  * `vec` and `buffer` are two distinct vectors (they come from mutable references)
        //     so their allocations cannot overlap.
        //  * `vec` is valid for reads because we have an exclusive reference to it and we
        //     checked the value of `i`.
        //  * `buffer` is valid for writes because we ensured we had enough memory to store
        //     `i` additional elements.
        std::ptr::copy_nonoverlapping(vec.as_ptr(), buffer.as_mut_ptr().add(buffer.len()), i);

        // Now the memory is moved.
        // we are not allowed to use it again from the `vec` vector.

        // We just extanded `buffer`, we need to update its length.
        // SAFEFY:
        //  * We ensured that the new length is less than the capacity (with `Vec::reserved`)
        //  * The vector is initialized for this new length (we moved the values).
        buffer.set_len(buffer.len() + i);

        // Now we need to update the first vector. The values from index `i` to its end
        // need to be moved at the begining of the vector.
        // SAFETY:
        //  * We have an exclusive reference to the vector. It is both valid for reads and writes.
        std::ptr::copy(vec.as_ptr().add(i), vec.as_mut_ptr(), i);

        // And update the length of `vec`.
        // SAFETY: This subtraction is safe because we previously checked that `vec.len() >= i`.
        vec.set_len(vec.len() - i);
    }
}

Note that I put buffer in the parameters of the function to avoid allocating a vector. If you want the same semantic as split_off, you can just do the following.

fn split_of_back<T>(vec: &mut Vec<T>, i: usize) -> Vec<T> {
    assert!(vec.len() >= i);
    
    let mut buffer = Vec::with_capacity(i);

    unsafe { /* same thing */ }

    buffer
}
like image 88
Gymore Avatar answered May 05 '26 10:05

Gymore



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!