I'm basically trying to convert an array of things into a user-defined struct with the same number of things:
fn some_func<T, const N: usize>(array: [SomeStruct; N]) -> T
And I'm trying to add a compile time check to the function signature like fn ... where size_of<T> == size_of<SomeStruct> * N, or fn some_func<T: SizeTimes<SomeStruct, N>, const N:usize>(...).
The conversion can be done with a union and ManuallyDrop, and a runtime check is trivial:
use std::mem::ManuallyDrop;
#[derive(Copy, Clone)]
pub struct SomeStruct { _inner: i32 }
union SomeUnion<T, const N: usize> {
    b: ManuallyDrop<T>,
    v: ManuallyDrop<[SomeStruct; N]>,
}
pub fn some_func<T, const N: usize>(array: [SomeStruct; N]) -> T {
    assert_eq!(
        size_of::<T>(), N * size_of::<SomeStruct>(), 
        "sizes are different: {}, {}",
        size_of::<T>(), N * size_of::<SomeStruct>()
    );
    let mut u = SomeUnion {
        v: ManuallyDrop::new(array),
    };
    unsafe { ManuallyDrop::take(&mut u.b) }
}
pub struct SomeContainer {
    _first_field: SomeStruct,
    _second_field: SomeStruct,
}
pub struct SomeOtherContainer {
    _first_field: SomeStruct,
    _second_field: SomeStruct,
    _third_field: SomeStruct,
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_correct() {
        let _a: SomeContainer = some_func([SomeStruct{_inner:0}; 2]);  // should compile ok
        let _b: SomeOtherContainer = some_func([SomeStruct{_inner:0}; 3]);  // should compile ok
    }
    #[test]
    fn test_should_not_compile() {
        let _c: SomeContainer = some_func([SomeStruct{_inner:0}; 3]);  // should fail compilation
        let _d: SomeContainer = some_func([SomeStruct{_inner:0}; 1]);  // should fail compilation
    }
}
but I can't manage to make a nice trait bound for this check. The closest I got was using a constant inside a generic struct:
// same code for SomeUnion, SomeStruct, SomeContainer and the tests
use std::marker::PhantomData;
pub struct RightSize<T, const N: usize> {
    pub t : PhantomData<T>,
}
impl<T, const N: usize> RightSize<T, N> {
    const SIZE_OK: () = assert!(size_of::<T>() == N * size_of::<SomeStruct>());
}
pub fn some_func<T, const N: usize>(array: [SomeStruct; N]) -> T {
    let _ = RightSize::<T, N>::SIZE_OK;   // <---- added this line to this function
    let mut u = SomeUnion {
        v: ManuallyDrop::new(array),
    };
    unsafe { ManuallyDrop::take(&mut u.b) }
}
Is there any way of doing fn some_func<T: SizeTimes<SomeStruct, N>(...)? Extra points if a custom message can be printed like ..."sizes differ: {}, {}", ...), and the instantiation place is mentioned in the compilation error (the line for let _b ... in the second test).
Tangentially, I guess the conversion could be done with transmute too? let me know if you think that's better.
Other things I tried:
EDIT: the comments and answer make a very good point that the layout is not guaranteed to be kept unless #[repr(C)] is used, so this approach should probably not be done in production code.
As far as I can see there is a conflict of goals...
As briefly outlined in the comment above, there is no way for some_func() to guarantee that the memory-layout of any generic U is equivalent to the memory-layout of [T; N], even if U is exactly N fields of T. The generic "container"-type U might have different alignment set for it, might have its fields reordered according to #[repr(Rust)], or might actually have extra fields that some_func did not expect. What is currently a transmute() in disguise via a union will lead to Undefined Behaviour if U does not adhere to the implicit rules set by some_func().
There are basically two conflicting goals:
some_func() should be generic enough that it can accept any U as the destination type. To be truly generic, we don't want to write down every possible U.some_func() implies the exact type-layout of U anyway. So we actually have to have an implicit set of types that adhere to what some_func() needs, and call that "generic".If only U "knows" its own exact type-layout, then it has to be U which type-converts [T; N], not the other way around. This leads us to good old fashioned std::convert::From, which will compile to a no-op as long as the containers' layout allows for that. One might want to add #[repr(C)] to any container-like type (which increases the chance that From will be a no-op), but we are not required to in order not to cause immediate UB.
pub struct SomeStruct {
    _inner: i32,
}
pub struct Container {
    _first: SomeStruct,
    _second: SomeStruct,
}
pub struct LargeContainer {
    _first: SomeStruct,
    _second: SomeStruct,
    _third: SomeStruct,
}
impl From<[SomeStruct; 2]> for Container {
    fn from(value: [SomeStruct; 2]) -> Self {
        let [_first, _second] = value;
        Self { _first, _second }
    }
}
impl From<[SomeStruct; 3]> for LargeContainer {
    fn from(value: [SomeStruct; 3]) -> Self {
        let [_first, _second, _third] = value;
        Self {
            _first,
            _second,
            _third,
        }
    }
}
fn main() {
    // This compiles
    let _a: Container = [SomeStruct { _inner: 0 }, SomeStruct { _inner: 1 }].into();
    // This compiles
    let _b: LargeContainer = [
        SomeStruct { _inner: 0 },
        SomeStruct { _inner: 1 },
        SomeStruct { _inner: 3 },
    ]
    .into();
    // This does not compile, because there is no impl for it.
    let _c: Container = [SomeStruct { _inner: 0 }].into();
}
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With