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