Rust的移动语义实际上是如何工作的?

3

在我编写了一些代码并阅读了一些文章后,我对Rust中的移动语义感到有些困惑。我认为值被移动后应该被释放,内存也应该失效。因此,我尝试编写一些代码来进行测试。

第一个示例

#[derive(Debug)]
struct Hello {
    field: u64,
    field_ptr: *const u64,
}

impl Hello {
    fn new() -> Self {
        let h = Hello {
            field: 100,
            field_ptr: std::ptr::null(),
        };
        h
    }

    fn init(&mut self) {
        self.field_ptr = &self.field as *const u64;
    }
}
fn main(){
    let mut h = Hello::new();
    h.init();
    println!("=================");
    println!("addr of h: ({:?}) \naddr of field ({:?})\nfield_ptr: ({:?}) \nptr value {:?}", &h as *const Hello, &h.field as *const u64, h.field_ptr, unsafe {*h.field_ptr});

    let c = &h.field as *const u64;
    let e = &h as *const Hello;
    let a = h;
    let d = &a.field as *const u64;

    println!("=================");
    println!("addr of a: ({:?}) \naddr of field ({:?})\nfield_ptr: ({:?}) \nptr value {:?}", &a as *const Hello, &a.field as *const u64, a.field_ptr, unsafe {*a.field_ptr});
    println!("=================");
    println!("addr of c {:?}\nvalue {:?}", c, unsafe {*c});
    println!("addr of d {:?}\nvalue {:?}", d, unsafe {*d});
    println!("addr of e {:?}\nvalue {:?}", e, unsafe {&*e});
}

以上代码的结果是:
=================
addr of h: (0x7ffee9700628) 
addr of field (0x7ffee9700628)
field_ptr: (0x7ffee9700628) 
ptr value 100
=================
addr of a: (0x7ffee9700720) 
addr of field (0x7ffee9700720)
field_ptr: (0x7ffee9700628) 
ptr value 100
=================
addr of c 0x7ffee9700628
value 100
addr of d 0x7ffee9700720
value 100
addr of e 0x7ffee9700628
value Hello { field: 100, field_ptr: 0x7ffee9700628 }

所以,我创建了一个自引用结构体Hello并将field_ptr指向u64字段,并使用裸指针保存结构体和字段的地址,然后我将h移动到a中使h变量无效,但我仍然可以通过裸指针获取原始变量的值,而我认为它不应该存在?

第二个例子

struct Boxed {
    field: u64,
}
fn main(){

   let mut f = std::ptr::null();
    {
        let boxed = Box::new(Boxed{field: 123});
        f = &boxed.field as *const u64;
    }
    println!("addr of f {:?}\nvalue {:?}", f, unsafe {&*f});
}

结果

addr of f 0x7fc1f8c05d30
value 123

我创建了一个包装值,并在使用后丢弃它,使用原始指针保存其地址,然后我仍然可以通过原始指针读取其字段的值。
因此,我的疑惑是:
1. 在Rust中,move操作实际上是memcpy吗?原始变量只是被编译器“隐藏”了吗? 2. Rust什么时候实际释放堆上变量的内存?(第二个示例)
谢谢!
我阅读过的内容: How does Rust provide move semantics?

4
请参考这个酒店比喻回答,了解为什么在释放内存后访问数据仍然可能看起来有效。在Miri下运行您的第二个示例可以捕获此使用-after-free错误,尽管我有点失望它没有捕获第一种情况。 - kmdreko
1
#1 是的,move 是一个伴随着所有权转移的 memcpy - 即旧所有者无法访问该值,新所有者负责释放它。当然,根据优化的情况,复制可能会被消除。#2 当值超出范围并调用 Drop::drop() 时,内存将被释放。这并不意味着内存已被清理,只是标记为未来分配而已(或更少见地,返回给操作系统供其他程序使用)。 - user4815162342
@user4815162342 谢谢你的回答,解决了我的问题。 - Sean
1个回答

4
你的输出的第一个块应该很清楚了,对吧?结构体的地址就是其在内存中的第一个位置的地址,这与其第一个字段的地址相同。
现在来看你的第二个块。你抓取了一些结构体的原始指针,然后通过 let a = h 移动了结构体。
这样做的结果是:在 上我们现在有了一个新变量 a,它是旧的变量 h 的堆栈布局的内存副本。这就是为什么 aa.field 都有一个新的地址。原始指针当然仍然指向旧的 h.field 地址,这就是你仍然能够访问那些数据的原因。
需要注意的是,你只能通过 unsafe 块来实现这一点,因为你所做的事情确实是不安全的。不能保证你的字段指针所指向的内容会保持有效。
如果你移除所有使用 unsafe 构造的代码,就没有办法通过 h.field 访问 a.field 了。
同样的思路适用于第二个示例。如果你不使用原始指针和不安全块,就无法访问已丢弃的内容,因为这段代码非常可疑。在你的简单示例中,它仍然有效,因为 Rust 并不会随意更改已经被丢弃的值的内存。除非程序中的其他部分重新利用了那块内存,否则它将保持原样。

在你的简单示例中,它仍然有效,因为Rust不会随意混淆已经被丢弃的值的内存。我认为这更多是LLVM的事情:从技术上讲,它应该能够考虑到时间上的变化,当分配堆栈帧时重用现有的堆栈插槽(如果它们的内容不再有效),但实际上要么它不费心,要么Rust没有标记变量为死亡,所以llvm必须假设它们都活着直到帧结束。 - Masklinn

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