Arrays are fixed-size. However, it is possible to initialize a new one with the contents of the other via some unsafe black magic.
Even though fixed-size arrays aren't as used as vectors or slices, these new methods could help replace some heap-allocated vectors & vecdeques by pure stack arrays. It's true linked lists would be a much better option than abusing these methods (because of the performance difference of writing a pointer vs 1-2 calls to memcpy()), but these are super useful for basic array combination & manipulation.
impl<T, const N: usize> [T; N] {
fn push<const L: usize>(self, array: [T; L]) -> [T; N + L] {
unsafe {
let mut result: [T; N + L] = zeroed(); // no real need to use MaybeUninit
let dst = &raw mut result; // get ptr
copy_nonoverlapping(&raw const self, dst.cast(), 1); // copy elements
let dst = &raw mut result[N..]; // offset ptr by N
copy_nonoverlapping(&raw const array, dst.cast(), 1); // copy elements
// avoid drop & deallocation of the copied elements
forget(self);
forget(array);
result
}
}
fn push_back<const L: usize>(self, array: [T; L]) -> [T; N + L] {
unsafe {
let mut result: [T; N + L] = zeroed(); // no real need to use MaybeUninit
let dst = &raw mut result; // get ptr
copy_nonoverlapping(&raw const array, dst.cast(), 1); // copy elements
let dst = &raw mut result[L..]; // offset ptr by L
copy_nonoverlapping(&raw const self, dst.cast(), 1); // copy elements
// avoid drop & deallocation of the copied elements
forget(self);
forget(array);
result
}
}
fn pop(mut self) -> [T; N - 1] {
if N == 0 {
panic!("Index out of bounds. This will become a compile-time error once control flow is supported on constant expressions.")
}
unsafe {
let mut result: [T; N - 1] = zeroed(); // no real need to use MaybeUninit
let src = &raw const self; // get ptr
copy_nonoverlapping(src.cast(), &raw mut result, 1); // copy elements
drop_in_place(&raw mut self[N - 1]); // drop popped element
forget(self); // avoid drop & deallocation of the copied elements
result
}
}
fn pop_back(mut self) -> [T; N - 1] {
if N == 0 {
panic!("Index out of bounds. This will become a compile-time error once control flow is supported on constant expressions.")
}
unsafe {
let mut result: [T; N - 1] = zeroed(); // no real need to use MaybeUninit
let src = &raw const self[1..]; // offset ptr by one
copy_nonoverlapping(src.cast(), &raw mut result, 1); // copy elements
drop_in_place(&raw mut self[0]); // drop popped element
forget(self); // avoid drop & deallocation of the copied elements
result
}
}
}
This is close to ArrayVec and similar types (except they mutate in place; an ArrayVec backed by a [T; N] starts with size 0 and fixed capacity N). There has been a lot of discussion about adding an ArrayVec to the standard library (basically the code is already there but only used internally right now). An immutable variant whose methods return some [T; N±K] might be more flexible in some use cases, but on the other hand is not implementable in stable right now unlike ArrayVec.
From an uninformed quick glance, ArrayVec looks like a wrapper over [MaybeUninit<T>; N] (or perhaps [Option<T>; N]), since it panics if the array is full. My implementation creates a new array of a different size.
Yes, I know, I alluded to the difference but could have been clearer. In many use cases it's fine to have a maximum size, especially given that if the exact size is encoded in the type, you anyway have a statically-enforced maximum size no matter what (ie. you can't push or pop conditionally or in a loop because the type changes every time).
The current implementation works fine with the current state of generic_const_exprs, but I'm not comfortable merging this until we can move that runtime bounds check to a compile-time if clause on the return type. The code would become something like:
fn pop$(_back)?(mut self) -> [T; if N > 0 { N - 1 } else { 0 }] {
if N == 0 { return [] }
...
}
Or if we can add where clauses:
fn pop$(_back)?(mut self) -> [T; N] where N > 0 {
...
}
As long as we can maintain the same signature & behavior as the one already implemented by generic_const_exprs I don't see any problem putting this behind a feature gate.
std just doesn't take things where the public interface depends on speculative features, even under feature gates.
You're free to make a nightly-only crate with these for now, and propose them for std once the necessary features are far enough along the stability path.
For popping, could we transmute to a MaybeUninit array, drop the popped elements, and then transmute back (at the right offset) to an array of smaller size? That avoids memcpy.
Also, using zeroed() could cause UB (consider if T is a reference, or a NonZero*) and I think MaybeUninit should be used (it also avoids an unnecessary memset).
I'll just note that, by analogy with Vec and slices, these methods should be named something like append and split, rather than push_back and pop_back. Maybe concat should be used instead of append.
Specifically, push and pop are used only when modifying a single element.
I'll tallocate some of my time (pun intended) on improving & adding more methods to the repository, with the hope of it being merged to core once constant exprs are mature enough (and maybe specialization too, in order to optimize non-drop & copy arrays).
I really dislike using const generics on the truncate methods, but the alternative is returning an unsized array [T] and then having the user transmute to a manually calculated size...