Move elements out of array in const context

I'm trying to figure out the preferred methods to:

  • move elements out of an array
  • destructure objects

on stable in a const context.

In the working example below we transpose the type of an array of structs into a struct of arrays.

The solutions I've come up with for moving elements out of an array is to:

  1. wrap the array in ManuallyDrop.
  2. ptr::read each element once.

Currently we can't destructure types in a const context despite them not implementing Drop. Instead of reading out the element and destructuring, we read out it's fields.

use std::{
    mem::{ManuallyDrop, MaybeUninit},
    ptr::addr_of,
};

pub struct S<T> {
    pub count: T,
    pub stride: T,
}

/// Transpose the type from an array of structs into a struct of arrays.
#[inline]
pub const fn into_soa<T, const N: usize>(shape: [S<T>; N]) -> S<[T; N]> {
    let mut stride = array_uninit();
    let mut count = array_uninit();

    let shape = ManuallyDrop::new(shape);

    // Work around our inability to obtain a reference to the inner value in a const context.
    let shape: &[S<T>; N] = manually_drop_inner_ref(&shape);

    const_for!(n in 0..N => {
        let shape = &shape[n];
        // SAFETY: The original will not be dropped and we only create one copy of each stride.
        stride[n].write(unsafe { addr_of!(shape.stride).read() });
        // SAFETY: The original will not be dropped and we only create one copy of each count.
        count[n].write(unsafe { addr_of!(shape.count).read() });
    });

    // SAFETY: All elements have been written to.
    let stride = unsafe { array_assume_init(stride) };
    // SAFETY: All elements have been written to.
    let count = unsafe { array_assume_init(count) };

    S { stride, count }
}

#[cfg(test)]
mod tests {
    use super::*;

    const_matches!(
        into_soa([S { stride: 1, count: 2 }, S { stride: 3, count: 4 }]),
        S {
            stride: [1, 3],
            count: [2, 4]
        }
    );
}

/// Create an array of uninitialized elements.
pub(crate) const fn array_uninit<T, const N: usize>() -> [MaybeUninit<T>; N] {
    [const { MaybeUninit::uninit() }; N] // <- is the const { ... } here necessary?
}

// TODO: Replace with stable version when stabilized.
pub(crate) const unsafe fn array_assume_init<T, const N: usize>(array: [MaybeUninit<T>; N]) -> [T; N] {
    // SAFETY: MaybeUninit<T> and T are guaranteed to have the same layout
    unsafe { ::core::mem::transmute_copy(&array) }
}

/// Provides access to the inner value of a ManuallyDrop<T>.
pub(crate) const fn manually_drop_inner_ref<T>(slot: &ManuallyDrop<T>) -> &T {
    // SAFETY: ManuallyDrop<T> and T are guaranteed to have the same layout
    unsafe { std::mem::transmute(slot) }
}

// inspired by crate const_for
#[macro_export]
macro_rules! const_for {
    ($var:pat_param in $range:expr => $body:stmt) => {
        let ::core::ops::Range { start: mut index, end } = $range;
        while index < end {
            let $var = index;
            $body
            index += 1;
        }
    };
}

// inspired by crate static_assertions
#[macro_export]
macro_rules! const_matches {
    ($expression:expr, $pattern:pat $(if $guard:expr)? $(,)?) => {
        const _: [(); 1] = [(); matches!($expression, $pattern $(if $guard)?) as usize];
    };
}

Is the code sound, and is there a better implementation?

I'm trying to develop some tools that make this sort of thing easier to write. For destructuring I've come up with the following:

/// Provides access to the inner value of a ManuallyDrop<T>.
pub(crate) const fn manually_drop_inner_ref<T>(slot: &core::mem::ManuallyDrop<T>) -> &T {
    // SAFETY: ManuallyDrop<T> and T are guaranteed to have the same layout
    unsafe { core::mem::transmute(slot) }
}

macro_rules! const_destructure_struct {
    (let $S:ident { $($field:ident: $var:ident),* } = $value:expr) => {
        let value = $value;
        let __destructures_all_fields_and_fields_are_unique = || {
            let $S { $($field: _),* } = &value;
        };
        let value = core::mem::ManuallyDrop::new($value);
        let value = manually_drop_inner_ref(&value);
        // SAFETY: We avoid double free by 1) only reading each field once (since they're unique) and 2) the original is wrapped in ManuallyDrop.
        $(
            let $var = unsafe { core::ptr::addr_of!(value.$field).read() };
        )*
    }
}

macro_rules! const_destructure_tuple {
    (let ($($var:ident),*) = $value:expr) => {
        const_destructure_tuple!(@impl ($($var),*); (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) => (); let () = $value);
    };
    (@impl (); ($($index_rest:tt),*) => ($($ty:tt),*); let ($($index:tt: $var:ident),*) = $value:expr) => {
        let value: ($($ty,)*) = $value; // asserts correct arity
        let value = core::mem::ManuallyDrop::new(value);
        let value = manually_drop_inner_ref(&value);
        // SAFETY: We avoid double free by 1) only reading each field once (since they're unique) and 2) the original is wrapped in ManuallyDrop.
        $(
            let $var = unsafe { core::ptr::addr_of!(value.$index).read() };
        )*
    };
    (@impl ($var_head:ident $(,$var_tail:ident)*); () => ($($ty:tt),*); let ($($index:tt: $var:ident),*) = $value:expr) => {
        compile_error!("tuple arity is larger than the maximum supported arity 12")
    };
    (@impl ($var_head:ident $(,$var_tail:ident)*); ($index_head:tt $(,$index_tail:tt)*) => ($($ty:tt),*); let ($($index:tt: $var:ident),*) = $value:expr) => {
        const_destructure_tuple!(@impl ($($var_tail),*); ($($index_tail),*) => ($($ty,)* _); let ($($index: $var,)* $index_head: $var_head) = $value);
    };
}

It's somewhat likely that what I'm doing is somehow unsound though, it would be good to have another pair of eyes on it.

Looking at the implementation of std::array::IntoIter, A by-value iterator, It looks like they move out of the array by notifying the compiler that each element is MaybeUninit, then doing a copy and ensuring the previous copy is never touched again so that the compiler can optimise it to a move.
It also looks like the relevent MaybeUninitare const too, we would just need to use the looping rather than range for the implementation to be const.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.