[u8; 1024 * 1024] or a Vec<u8>

I like looking at [u8; 1024 * 1024] over Vec<u8> because right in the source code, I know for a fact the size of this object is constant and will not change.

I don't like [u8; 1024 * 1024] because it is begging for stackoverflows, especially on wasm32.

Is there something like a Vec<u8> but we hard code its length to a (at compile time) known constant ? Current approach is:

pub struct Foo<const N: usize> {
  inner: Vec<u8>
}

and the only forward/export some of the functions defined on Vec<u8>, but I am wondering if there is a cleaner solution.

That will work ok, but if you make it much bigger you will get a stack overflow.
I suggest using only "small" arrays.

Sorry, the question was not intended to be: "is this okay";

the question is intended to be: "I am going to use multiple of these, of various sizes, thus I need a 'const sized Vec' -- what is the best way to do this?" :slight_smile:

I suggest just using a Vec, rather than obfuscating. A question of style, depends a bit if you expect anyone else to ever be reading the code.

You mean something like Box<[u8; N]>?

By Box's Deref it can do anything that [u8; N] can, but it is stored on the heap like Vec<u8> is.

9 Likes

Tom's answer is the best way. The Box itself is only 1 pointer in size, compared to Vec's 3. You'd make it by creating a Vec, then converting it to Box.

pub fn make_boxed_slice() -> Box<[u8; 1024 * 1024]> {
    vec![0; 1024 * 1024].try_into().unwrap()
}

You can't use Box::new because that may put it on the stack briefly, especially in debug mode.

Edit: removed .into_boxed_slice()

9 Likes

You can get lucky with emplacement but it's not guaranteed.

You can convert from Vec to Box without reallocation if there's no spare capacity; if there's spare capacity it'll reallocate.

A boxed slice is the size of two pointers; a boxed array is one.

1 Like

Ah, thanks; this is the missing piece.

Since 1.66(released at the end of the 2022) the .into_boxed_slice() part is not mandatory.

1 Like

Here's a generic version

pub fn boxed_slice<T: Default + Clone, const N: usize>() -> Box<[T; N]> {
    match vec![Default::default(); N].into_boxed_slice().try_into() {
        Ok(r) => r,
        Err(_) => unreachable!()
    }
}

Or leave away the into_boxed_slice as @Hyeonu suggested.

2 Likes

I was trying to drop the Default/Clone requirement, and created

pub fn make_boxed_slice<T, const N: usize, F: Fn(usize) -> T>(f: F) -> Box<[T; N]> {
    let mut v = Vec::with_capacity(N);
    for i in 0..N {
        v.push(f(i));
    }
    match v.try_into() {
        Ok(r) => r,
        Err(_) => panic!(""),
    }
}

interested on feedback if this can be improved

Fyi, from the docs of vec

vec![x; n] , vec![a, b, c, d] , and Vec::with_capacity(n), will all produce a Vec with exactly the requested capacity. If len == capacity , (as is the case for the vec! macro), then a Vec<T> can be converted to and from a Box<[T]> without reallocating or moving the elements.

So no reallocation with
vec![...].into_boxed_slice().

1 Like

Hmm, so would the following code golf be a bad idea ?

pub fn make_boxed_slice<T, const N: usize, F: Fn(usize) -> T>(f: F) -> Box<[T; N]> {
    match (0..N).into_iter().map(f).collect::<Vec<_>>().try_into() {
        Ok(r) => r,
        Err(_) => panic!(""),
    }
}

because we lose with the ::with_capacity call ?

The stdlib code regarding this part is pretty complicated due to heavy optimizations including specialization, but at least for now you don't loose the ::with_capacity() call. Typically perf things like this is hardly guaranteed but there's no reason to not exploit them. Always setup perf regression test if you do care them.

"If length equals capacity", just as I said.

collecting into a Vec uses the size_hint from the iterator -- that's basically the whole point of size_hint.

Then the question is whether the size_hint is perfect or not, which can sometimes be complex but in this case it's easy: Range<usize> is ExactSizeIterator and Map<I> is ExactSizeIterator when I is, so that collect will certainly pre-allocate the correct amount.

But of course you don't even need to do that logic, because you can just collect into a Box<[_]> directly:

pub fn make_boxed_slice<T, const N: usize, F: Fn(usize) -> T>(f: F) -> Box<[T; N]> {
    (0..N).map(f).collect::<Box<[_]>>().try_into().ok().unwrap()
}
5 Likes

wtf; did not know this was even possible :slight_smile:

It's made possible by this impl<I> FromIterator<I> for Box<[I]>. I find looking at the list of FromIterator implementations useful when trying to work out what I can collect() into :slight_smile:

1 Like

I think you can get rid of the .ok() here:

pub fn make_boxed_slice<T, const N: usize, F: Fn(usize) -> T>(f: F) -> Box<[T; N]> {
    (0..N).map(f).collect::<Box<[_]>>().try_into().unwrap()
}

Then you need T: Debug.