为什么通过原始指针修改可变引用的值不违反Rust的别名规则?

9

我对 Rust 的别名规则并没有非常牢固的理解(而且据我所知,它们并没有得到很好的定义),但我不明白为什么在 std::slice 文档中 这个代码示例 是正确的。我在此重复这段代码:

let x = &mut [1, 2, 4];
let x_ptr = x.as_mut_ptr();

unsafe {
    for i in 0..x.len() {
        *x_ptr.offset(i as isize) += 2;
    }
}
assert_eq!(x, &[3, 4, 6]);

我在这里看到的问题是,x 是一个 &mut 引用,编译器可以假定它是唯一的。通过 x_ptr 修改了 x 的内容,然后通过 x 读回来,我认为编译器没有理由认为 x 已经被修改,因为它从未通过唯一存在的 &mut 引用进行修改。
那么,我错过了什么?
  • 编译器是否必须假定 *mut T 可能与 &mut T 别名,即使通常可以假定 &mut T 永远不会别名另一个 &mut T

  • unsafe 块是否充当某种别名障碍,其中编译器假定其内部的代码可能已修改范围内的任何东西?

  • 这个代码示例有问题吗?

如果有某种稳定的规则使这个示例没问题,那么它是什么?它的范围是什么?我应该担心别名假设会破坏 unsafe Rust 代码中的随机事物吗?


我认为这是由LLVM处理的,因为xx_ptr包含相同类型的地址,LLVM必须重新加载x - Stargateur
@Stargateur 真的吗?我认为基于类型的别名分析使得LLVM能够更强地假设内存中相同类型对象的不相交性。 - lcmylin
1
@Mylin:从记忆来看,TBAA是选择加入的(前端需要发出特定属性),而rustc则不是选择加入。相反,它使用每个变量的注释。 - Matthieu M.
实际上,Rust不会根据指针类型进行任何推理(除了检查内部可变性)。因此,@Stargateur所写的对于Rust来说是不正确的。 - Ralf Jung
1个回答

9

免责声明:目前还没有正式的内存模型。1

首先,我想解决以下问题:

我在这里看到的问题是,编译器可以假定 x 是唯一的 &mut 引用。

是的...也不是。只有当 x 没有被借用时,才能假定它是唯一的,这是一个重要的区别:

fn doit(x: &mut T) {
    let y = &mut *x;
    //  x is re-borrowed at this point.
}

因此,目前我会假设从x派生指针在某种意义上暂时“借用”了x
当然,在没有正式模型的情况下,这一切都是含糊不清的。这也是为什么rustc编译器还不会过于积极地进行别名优化的部分原因:在定义正式模型并检查代码是否符合它之前,优化必须保守。
RustBelt项目旨在为Rust建立一个经过正式证明的内存模型。Ralf Jung的最新消息是关于Stacked Borrows model的。

来自 Ralf(评论):以上示例的关键点在于存在清晰的从 xx_ptr,再回到 x 的转移。因此,x_ptr 在某种意义上是一个作用域借用。如果使用顺序为 xx_ptr、再回到 x 和再回到 x_ptr,则后者将会是未定义行为:

fn main() {
    let x = &mut [1, 2, 4];
    let x_ptr = x.as_mut_ptr(); // x_ptr borrows the right to mutate

    unsafe {
        for i in 0..x.len() {
            *x_ptr.offset(i as isize) += 2; // Fine use of raw pointer.
        }
    }
    assert_eq!(x, &[3, 4, 6]);  // x is back in charge, x_ptr invalidated.

    unsafe { *x_ptr += 1; }     // BÄM! Used no-longer-valid raw pointer.
}

2
确实,关键点在于x_ptr派生自 x, 而且自从x_ptr被创建以来就没有使用过x。 这两个条件必须同时满足才能使这段代码正确。 - Ralf Jung
@RalfJung 虽然它是 """不允许""" 的,但是在最后一行失败后 assert_eq!(x, &[3, 4, 6]); 会告诉你已经变成了 4, 4, 6... 所以我们又回到了 Rust 构建时要避免的问题,即简单地不定义什么是正确的和什么是不正确的吗?如果编译器没有进行优化(我在发布模式下构建了它,情况是相同的),现在就不能修复它,那么这些任意规则还有什么意义呢?当无法弄清楚哪里出了问题,哪些操作是可行的时,这对我来说是一个重大痛点... - user11877195
要完全清楚:&mut + &mut = compile error,这是唯一显而易见的事情...我通过尝试弄清楚&mut + *mut = wrong(在这里我们声称它是错误的)以及*mut + *mut = wrong是否正确来到了这里(我还没有找到任何提到它的东西)。如果还没有明确的规则,那么这是UB,但不是UB(tm)吗? - user11877195
1
它是UB,并在某些情况下被利用。由于LLVM的错误,一些优化暂时被禁用。但仅仅因为编译器目前不能以最大可能的方式识别您的代码,这并不意味着它将来不会变得更好。您不能指望编译器从一开始就执行所有最激进的优化。在C中,通常的方法似乎是“优化直到有人抱怨它是错误的”;我们希望首先确信我们知道什么是正确的。 - Ralf Jung
1
@Sahsahae 此外,不安全的 Rust 确实与 C 和 C++ 共享许多未定义行为的问题。Rust 的价值在于能够将不安全性封装在抽象后面,并将其局部化。比较 C++ 中的 std::vector 和 Rust 中的 Vec:它们的 实现 非常相似,并且在两种语言中同样危险。但作为一个 用户,有一个巨大的区别:在 C++ 中,你必须一直担心迭代器失效等问题,在 Rust 中,你知道编译器会保护你。 - Ralf Jung

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