Move elements out of array in const context

⚓ Rust    📅 2025-10-07    👤 surdeus    👁️ 5      

surdeus

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?

1 post - 1 participant

Read full topic

🏷️ Rust_feed