11

How would you go about creating a stack-allocated vector-like container with some fixed upper limit on the number of elements it can contain? You can see my attempt at this below, but it doesn't compile:

// The following is at crate level
#![feature(unsafe_destructor)]

use std::mem;
use std::ptr;
use std::slice::Iter;

pub struct StackVec<T> {
    buf: [T; 10],
    len: usize,
}

impl<T> StackVec<T> {
    pub fn new() -> StackVec<T> {
        StackVec {
            buf: unsafe { mem::uninitialized() },
            len: 0,
        }
    }

    pub fn iter(&self) -> Iter<T> {
        (&self.buf[..self.len]).iter()
    }

    pub fn push(&mut self, value: T) {
        unsafe { ptr::write(self.buf.get_mut(self.len).unwrap(), value); }
        self.len += 1;
    }

    pub fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            None
        } else {
            unsafe {
                self.len -= 1;
                Some(ptr::read(self.buf.get(self.len).unwrap()))
            }
        }
    }
}

#[unsafe_destructor]
impl<T> Drop for StackVec<T>
    where T: Drop
{
    fn drop(&mut self) {
        for elem in self.iter() {
            unsafe { ptr::read(elem); }
        }
        unsafe { mem::forget(self.buf); } // ERROR: [1]
    }
}

This is the compile-time error I get:
[1] error: cannot move out of type stackvec::StackVec<T>, which defines the Drop trait

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
kmky
  • 783
  • 6
  • 17
  • 1
    You may be interested in [another question with the same goal](http://stackoverflow.com/questions/29196869/how-to-change-a-value-while-dropping) – Shepmaster Mar 24 '15 at 18:23
  • @Shepmaster I don't think the question you suggested is on the same problem. – Dmitry Belyaev Mar 25 '15 at 07:33
  • @DmitryBelyaev both questions want to create a off-the-heap vector with a maximum size. Both questions ultimately deal with how to drop the partially filled array. – Shepmaster Mar 25 '15 at 11:06
  • @Shepmaster It's **a** solution. Just not a very optimal one. If I were to wrap the fixed-size array into an `Option`, then every time I index into it (or do anything with it really) I would have to check whether it's `None` or not. It seems too much of an overhead for something that's supposed to be the fastest possible kind of container. – kmky Mar 25 '15 at 11:42
  • It did occur to me though, that it's not that beneficial to use a stack allocated array (versus a `Vec`) for values of type `T` which implement `Drop` (and therefore are not `Copy` types). Most of the time those kinds of types allocate on the heap, so you end up having just the pointers on the stack and the actual data scattered along the heap anyway. And it's quite easy to implement this `StackVec where T: Copy`. – kmky Mar 25 '15 at 11:54
  • But if it's actually impossible to make a fast and general purpose stack-allocated vector-like container, then to me this seems like a language defect. So therefore I would like to find a good solution to this. – kmky Mar 25 '15 at 12:09
  • Just an idea: how about invoking `alloca(3)` directly using `unsafe` block? – swizard Mar 25 '15 at 19:05
  • Here's a very cool trick: You don't need to impl the method `iter` or any other slice method. Just impl `Deref` and `DerefMut` (`Target=[T]`) and you get all slice methods for free. – bluss May 19 '15 at 01:56

2 Answers2

11

I've written an implementation, and I'll go over the highlights.

  • Full code is available at crates.io/arrayvec (API doc)

  • Use a trait (called Array) to abstract over different array sizes. It needs to provide raw pointers so that we can use the array as backing storage.

/// Trait for fixed size arrays.
pub unsafe trait Array {
    /// The array's element type
    type Item;
    unsafe fn new() -> Self;
    fn as_ptr(&self) -> *const Self::Item;
    fn as_mut_ptr(&mut self) -> *mut Self::Item;
    fn capacity() -> usize;
}
  • In contemporary rust style, we can only implement this trait for specific array sizes. I cover some small sizes with a macro:
macro_rules! fix_array_impl {
    ($len:expr ) => (
        unsafe impl<T> Array for [T; $len] {
            type Item = T;
            /// Note: Returning an uninitialized value here only works
            /// if we can be sure the data is never used. The nullable pointer
            /// inside enum optimization conflicts with this this for example,
            /// so we need to be extra careful. See `Flag` enum.
            unsafe fn new() -> [T; $len] { mem::uninitialized() }
            fn as_ptr(&self) -> *const T { self as *const _ as *const _ }
            fn as_mut_ptr(&mut self) -> *mut T { self as *mut _ as *mut _}
            fn capacity() -> usize { $len }
        }
    )
}

macro_rules! fix_array_impl_recursive {
    () => ();
    ($len:expr, $($more:expr,)*) => (
        fix_array_impl!($len);
        fix_array_impl_recursive!($($more,)*);
    );
}

fix_array_impl_recursive!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
                          16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
                          32, 40, 48, 56, 64, 72, 96, 128, 160, 192, 224,);
  • We need to suppress the default drop of the embedded array. You can do this by in theory using Option<Array> and using ptr::write to overwrite it with None at the last moment in Drop.

  • We must however use our own enum, similar to Option for one reason: We need to avoid non-nullable pointer optimization that applies to enums that have the same representation as Option. Then in Drop we do the crucial inhibition of the inner array's default destructor: we forcibly overwrite our enum. Only after destructing all the elements, of course.

/// Make sure the non-nullable pointer optimization does not occur!
#[repr(u8)]
enum Flag<T> {
    Dropped,
    Alive(T),
}

/// A vector with a fixed capacity.
pub struct ArrayVec<A: Array> {
    len: u8,
    xs: Flag<A>,
}

impl<A: Array> Drop for ArrayVec<A> {
    fn drop(&mut self) {
        // clear all elements, then inhibit drop of inner array
        while let Some(_) = self.pop() { }
        unsafe {
            ptr::write(&mut self.xs, Flag::Dropped);
        }
    }
}
  • We implement Deref<Target=[T]> and DerefMut and get tons of slice methods for free. This is a great feature of Rust!
impl<A: Array> Deref for ArrayVec<A> {
    type Target = [A::Item];
    fn deref(&self) -> &[A::Item] {
        unsafe {
            slice::from_raw_parts(self.inner_ref().as_ptr(), self.len())
        }
    }
}
  • The ArrayVec type has an invariant, that the Flag<A> is always Flag::Alive(A) when the value is alive. We should be able to optimize with this in mind. (A FIXME is marked there.)
fn inner_mut(&mut self) -> &mut A {
    // FIXME: Optimize this, we know it's always present.
    match self.xs {
        Flag::Alive(ref mut xs) => xs,
        _ => unreachable!(),
    }
}

Thank you kmky for asking question! Exploring this answer led to the creation of arrayvec linked above, and uncovered some of the points that were very important to have it be a safe rust data structure.

bluss
  • 12,472
  • 1
  • 49
  • 48
1

My guess is that the compiler doesn't know which elements of the array are "free" and which need a destructor to run when the array is dropped.

Try storing Option<T>, which has a .take() method that will allow you to move an element out of the array.

Kornel
  • 97,764
  • 37
  • 219
  • 309