Rust中结构体字段的多态更新

3
假设我有一个多态类型 T<A>:
#[repr(C)]
pub struct T<A> {
    x: u32,
    y: Box<A>,
}

以下是我的推理:
  • 根据 std::boxed的内存布局部分:

    只要 T: SizedBox<T> 就保证会被表示为单个指针,并且与 C 指针(即 C 类型 T*)ABI 兼容。

    这意味着无论 A 是什么,y 应该具有相同的布局;

  • 考虑到 T 上的 #[repr(C)] 属性, 我预计对于所有的 A, T<A> 将共享相同的布局;

  • 因此,我应该能够原地修改 y,甚至赋值不同类型的值 Box<B>

我的问题是以下代码是否格式良好或存在未定义的行为?(下面的代码已被编辑。)
fn update<A, B>(t: Box<T<A>>, f: impl FnOnce(A) -> B) -> Box<T<B>> {
    unsafe {
        let p = Box::into_raw(t);
        let a = std::ptr::read(&(*p).y);
        let q = p as *mut T<B>;
        std::ptr::write(&mut (*q).y, Box::new(f(*a)));
        Box::from_raw(q)
    }
}

注意:

上述代码旨在进行多态更新,以便x字段保持不变。假设x不仅仅是一个u32,而是一些非常大的数据块。整个想法是改变y的类型(以及其值),而不影响字段x


正如 Frxstrem 指出的那样,下面的代码确实会导致未定义的行为。我犯了一个愚蠢的错误:我忘记了为由 f 产生的 B 重新分配内存。看起来 上面的新代码通过了 Miri 检查
fn update<A, B>(t: Box<T<A>>, f: impl FnOnce(A) -> B) -> Box<T<B>> {
    unsafe {
        let mut u: Box<T<std::mem::MaybeUninit<B>>> = std::mem::transmute(t);
        let a = std::ptr::read::<A>(u.y.as_ptr() as *const _);
        u.y.as_mut_ptr().write(f(a));
        std::mem::transmute(u)
    }
}

2
Miri说这是未定义的行为。 - Frxstrem
我造成了一个段错误。 - Aiden4
@Frxstrem 我已经更新了代码。之前的代码有误,因为它重复使用了 A 的堆内存来创建新对象 B。新代码通过了 Miri 检查。但这并不意味着它是100%正确的,对吧? - Ruifeng Xie
@RuifengXie 没错。Miri可以找到很多错误,但不能保证它能找到所有的错误。 - Peter Hall
1
继SebastianRedl所说的,如果f发生恐慌,它会泄漏T的分配。您在修改后的unsafe代码中似乎也在其他地方做得很好。 - Aiden4
显示剩余3条评论
1个回答

1
你可能会被整个 "T" 事情搞糊涂。这应该更容易分析。具体来说,请阅读这里
use core::mem::ManuallyDrop;
use core::alloc::Layout;

unsafe trait SharesLayout<T: Sized>: Sized {
    fn assert_same_layout() {
        assert!(Layout::new::<Self>() == Layout::new::<T>());
    }
}

/// Replaces the contents of a box, without reallocating.
fn box_map<A, B: SharesLayout<A>>(b: Box<A>, f: impl FnOnce(A) -> B) -> Box<B> {
    unsafe {
        B::assert_same_layout();
        let p = Box::into_raw(b);
        let mut dealloc_on_panic = Box::from_raw(p as *mut ManuallyDrop<A>);
        let new_content = f(ManuallyDrop::take(&mut *dealloc_on_panic));
        std::mem::forget(dealloc_on_panic);
        std::ptr::write(p as *mut B, new_content);
        Box::from_raw(p as *mut B)
    }
}

然后简单地:
unsafe impl<A, B> SharesLayout<T<A>> for T<B> {}

fn update<A, B>(bt: Box<T<A>>, f: impl FnOnce(A) -> B) -> Box<T<B>> {
    box_map(bt, |t| {
        T { x: t.x, y: Box::new(f(*t.y))}
    })
}

我理解了SharesLayout特质的作用,以及DeallocOnDrop如何防止内存泄漏。但是如果x的大小很大,那么新的update是否保证避免复制字段x - Ruifeng Xie
@RuifengXie 嗯,它被移动了,这可能会被编译器优化,我不确定。请注意,任何地方都没有 Copy 特质限制。 - orlp
是的,当然不是 Rust 中所指的复制。我指的是 memcpy。我相信在大多数情况下,它会被优化掉,但如果 box_map 函数没有内联,那么优化就变得不可能了。 - Ruifeng Xie
1
@RuifengXie 我再次更新了实现,消除了对 DeallocOnDrop 的需求。 - orlp

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