使用const generics将可变指针数组转换为可变引用

3
假设我通过未指定的方式来保证,我有一个类型为 [*mut T; N] 的数组,其中包含 N有效且不重叠的可变指针,所有指针的生命周期都是 'a。那么我如何将其转换为可变引用数组 [&'a mut T; N]
换句话说,我该如何实现这个函数?
unsafe fn ptrs_to_refs<'a, T, const N: usize>(ptrs: [*mut T; N]) -> [&'a mut T; N] {
    // What goes here?
}

我不是特别希望使用分配步骤的实现(例如使用Vec)。


由于min_const_generics在Rust 1.51中被设置为稳定版本,因此本问题针对的是一个假设的稳定版本Rust,该版本同时也拥有min_const_generics。我正在寻找与这些要求兼容的答案,不使用其他不稳定的功能。

例如,使用特性array_map可以简单地实现。

unsafe fn ptrs_to_refs<'a, T, const N: usize>(ptrs: [*mut T; N]) -> [&'a mut T; N] {
   arr.map(|ptr| unsafe { &mut *ptr })
}

但是,由于我不知道它是否设定为稳定版本1.51(据我所知),我不想使用它。

3个回答

2
理想情况下,您可以在此处使用std::mem::transmute,因为引用和指针具有相同的内存布局。不幸的是,即使两种类型具有相同的布局,std::mem::transmute也无法处理通用数组。但是,您可以使用union来解决这个问题,因为使用#[repr(C)] union类似于transmute
#[repr(C)]
union Transmute<'a, T, const N: usize> {
    ptr_arr: ManuallyDrop<[*mut T; N]>,
    ref_arr: ManuallyDrop<[&'a mut T; N]>,
}

unsafe fn ptrs_to_refs<'a, T, const N: usize>(ptrs: [*mut T; N]) -> [&'a mut T; N] {
    let u = Transmute::<'a, T, N> { ptr_arr: ManuallyDrop::new(ptrs) };
    ManuallyDrop::into_inner(u.ref_arr)
}

你也可以使用std::mem::transmute_copy,在这里它实际上做的是相同的事情:

unsafe fn ptrs_to_refs<'a, T, const N: usize>(ptrs: [*mut T; N]) -> [&'a mut T; N] {
    std::mem::transmute_copy(&ptrs)
}

2
我会首先考虑使用 std::mem::transmute,但目前这个方法不可用;详见常量泛型:通用数组转换无法工作 #61956
与此同时,你可以使用联合体作为解决方法,因为指针和引用具有相同的布局
use core::mem::ManuallyDrop;

unsafe fn ptrs_to_refs<'a, T, const N: usize>(ptrs: [*mut T; N]) -> [&'a mut T; N] {
    union X<'a, T, const N: usize> {
        a_raw: ManuallyDrop<[*mut T; N]>,
        a_ref: ManuallyDrop<[&'a mut T; N]>,
    }

    let a_raw = ManuallyDrop::new(ptrs);
    let x = X { a_raw };
    ManuallyDrop::into_inner(x.a_ref)
}

您还可以使用transmute_copy来达到同样的效果:
unsafe fn ptrs_to_refs<'a, T, const N: usize>(ptrs: [*mut T; N]) -> [&'a mut T; N] {
    std::mem::transmute_copy(&ptrs)
}

将指针转换为引用(假设指针有效且结果不与另一个引用别名)是否保证安全?我目前的想法是重新借用到MaybeUninit数组中,然后将MaybeUninit复制到其他地方(这是保证安全的)。 - orlp
我相信如果这个答案是正确的,并且直接在数组之间转换是安全的,那么用std::mem::transmute_copy(&ptrs)替换整个代码块应该可以工作。 - orlp
@orlp 看起来Frxstrem的答案链接到参考资料,说明布局是相同的,这就使得转换是可以的。我原以为我先尝试了transmute_copy但失败了,但现在我确实看到它可以工作了... - Shepmaster
“Same layout”是否意味着如果它们指向同一对象,则位表示也相同?对齐又如何?对齐也很重要,因为我们在考虑数组。 - orlp
1
@orlp 是的,根据我的阅读,它将具有相同的对齐方式和填充。 - Shepmaster

2

以下内容是从MaybeUninit文档中的示例修改而来:

unsafe fn ptrs_to_refs<'a, T, const N: usize>(ptrs: [*mut T; N]) -> [&'a mut T; N] {
    // Create an uninitialized array of `MaybeUninit`. The `assume_init` is
    // safe because the type we are claiming to have initialized here is a
    // bunch of `MaybeUninit`s, which do not require initialization.
    let mut refs: [MaybeUninit<&'a mut T>; N] = MaybeUninit::uninit().assume_init();
    
    // Dropping a `MaybeUninit` does nothing. Thus using raw pointer
    // assignment instead of `ptr::write` does not cause the old
    // uninitialized value to be dropped.
    for i in 0..N {
        refs[i] = MaybeUninit::new(&mut *ptrs[i]);
    }
    
    std::mem::transmute_copy::<_, [&'a mut T; N]>(&refs)
}

这个答案并不依赖于将[*mut T; N]转换为[&mut T; N]是否安全,而仅仅依赖于在正确初始化后将MaybeUninit<T>转换为T


我本想提到这一点,但那个循环让我感到很悲伤。可以这样做 for (a, b) in refs.zip(ptrs) { *a = b } - Shepmaster
@Shepmaster 这需要使用 array_zip,但它不是稳定的。此外,这样我可以确定没有任何额外的神奇复制/移动/删除操作会违反安全性。在某些情况下,使用惯用的 Rust 可能更好,但在这些深处的野兽中,我将坚持我可以立即看到是安全的东西。 - orlp
你还缺少什么? - Shepmaster
@Shepmaster 那样做可以,但我仍然不同意它更易读的说法。 - orlp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接