Add `push`, `push_back`, `pop` & `pop_back` methods to fixed-size arrays

One implementation possibility is to have the slice-returning version and then just wrap it in a const-generic version which does the TryFrom.

That's the same as (&array[..index]).try_into(). I'm pretty sure that try_into() also performs a memcpy(), so the behavior is not that different from truncate.

Playground

Any time you have a by-value function argument/return, it's logically a copy, and you're at the mercy of copy elision to remove them. There's nothing more you can do.

Btw, here's a trick to optimize concat that also makes it const-compatible:

const fn concat<T, const N: usize, const M: usize>(
   lhs: [T; N], rhs: [T; M],
) -> [T; { N + M }] {
    #[repr(C)]
    struct Concat<LHS, RHS>(LHS, RHS);

    unsafe { transmute(Concat(lhs, rhs)) }
}

and I'd currently write truncate as

fn truncate<T, const LESS: usize, const MORE: usize>(
    arr: [T; MORE],
) -> [T; LESS]
where
    const { LESS <= MORE },
{
    let arr = ManuallyDrop::new(arr);
    // optional: to avoid leaking at greater risk of abort:
    // consider also a manual guard defuse
    let arr = scopeguard::guard_on_unwind(arr, |arr| {
        // NB: tail is already dropped
        unsafe { drop_in_place(&mut arr[..LESS]) };
    });
    unsafe {
        drop_in_place(&mut arr[LESS..]);
        transmute_copy(&arr)
    }
}

(I don't know if where const is implemented yet.)

Or perhaps alternatively to make the drop semantics more standard via being implicit,

fn split_at<T, const MID: usize, const LEN: usize>(
    arr: [T; LEN],
) -> ([T; MID], [T; { LEN - MID }])
where
    const { MID <= LEN },
{
    let arr = ManuallyDrop::new(arr);
    // TODO: check if unwrap_unchecked is needed
    let lhs = arr[..MID].try_into().unwrap();
    let rhs = arr[MID..].try_into().unwrap();
    (lhs, rhs)
}

fn truncate<T, const LESS: usize, const MORE: usize>(
    arr: [T; MORE],
) -> [T; LESS]
where
    const { LESS <= MORE },
{
    split::<T, LESS, MORE>(arr).0
}

I really like these implementations, I'll implement them in the repo (with some minor changes).

Looks like we can't implement concat like that...

I guess transmute_copy() would do the work as it doesn't check the sizes.

this works

I based that off of memory from a version that concatenates const strings. Pulling it up, it does use a custom transmute_prefix to sidestep the dependent sizes check.

I see you're doing impl<T> and impl<T: Drop> — this is absolutely not what you want. A T: Drop bound is never what you want.

As a trait bound, T: Drop holds when an explicit implementation of Drop exists. If you have something like struct S { list: Vec<Data> } and no explicit impl Drop for S, then S: Drop does not hold.

If you want to specialize behavior for types without drop glue, you need to use T: Copy (for a static guarantee) or needs_drop (for a monomorphized check).

But for this case, just call drop_in_place::<[T]>. It will trivially monomorphize to nothing when instantiated for a T which is !needs_drop::<T>(). (If you're extremely paranoid, call drop_in_place::<[T; N]>.)

3 Likes

Hmmmm, I specialized over Drop because drop_in_place() is not const and everything else was, but I think I can invert the implementations and specialize over Copy, defaulting to the Drop impl.

The pop operation is already possible with destructuring:

let [..rest, tail] = array;

I don't know how efficient this is compared to the proposed method, but if it isn't as efficient, I think it can be further optimized.

For the push operation, I'd much rather have a built-in syntax that looks symmetrical to destructuring:

let new_array = [..array, new_elem];

In JS, this is called spread syntax. Rust has something similar (the struct update syntax), but currently only for structs and enum variants.

2 Likes

It's worth noting that this causes a lot of issues, because the pattern spread syntax is the same as range constructor syntax.

So [..array, new_elem] is [RangeTo::new(array), new_elem].

That's not to say that this isn't resolvable, but it is an issue.

1 Like

Maybe we can do 3 dots like this for append:

let array = [...old_array, element1, element2];

And this for concat:

let array = [...old_array1...old_array2, element1, element2];`

With the possibility of combining them:

let array = [...old_array1...old_array2...old_array3, element1, element2, element3];`

For truncate we have destructuring assigments.

Once i get home I'll power on my laptop and write a language design post.

It looks like you could replace both methods with a generic implementation of std::ops::Add<[T;N]> for [T;L].

It has already been discussed in the thread linked above, such implementation would be ambiguous between concatenation and elementwise addition.

Huh, and right above my post too... Looks like something glitched - I only saw the post before that one.