对于共享引用和可变引用,语义很明确:as

所以这个代码:

#[no_mangle]
pub extern fn run_ref(a: &i32, b: &mut i32) -> (i32, i32) {
    let x = *a;
    *b = 1;
    let y = *a;
    (x, y)
}

编译(在x86_64上)为:

run_ref:
    movl    (%rdi), %ecx
    movl    $1, (%rsi)
    movq    %rcx, %rax
    shlq    $32, %rax
    orq     %rcx, %rax
    retq

请注意,a指向的内存只读取一次,因为

原始指针更复杂.原始指针算法和强制转换是

我们可以将原始指针转换回共享和可变的引用

但是如果我们直接使用原始指针,语义是什么呢?

#[no_mangle]
pub unsafe extern fn run_ptr_direct(a: *const i32, b: *mut f32) -> (i32, i32) {
    let x = *a;
    *b = 1.0;
    let y = *a;
    (x, y)
}

汇编至:

run_ptr_direct:
    movl    (%rdi), %ecx
    movl    $1065353216, (%rsi)
    movl    (%rdi), %eax
    shlq    $32, %rax
    orq     %rcx, %rax
    retq

虽然我们写了一个不同类型的值,但第二次读取仍然进行

请注意,一个普通的优化C/C++编译器将消除第二个问题

struct tuple { int x; int y; };

extern "C" tuple run_ptr(int const* a, float* b) {
    int const x = *a;
    *b = 1.0;
    int const y = *a;
    return tuple{x, y};
}

汇编至:

run_ptr:
    movl    (%rdi), %eax
    movl    $0x3f800000, (%rsi)
    movq    %rax, %rdx
    salq    $32, %rdx
    orq     %rdx, %rax
    ret

Playground with Rust code examples

godbolt Compiler Explorer with C example

那么:如果我们直接使用原始指针,语义是什么呢

这对编译器是否被允许有直接影响

推荐答案

No awkward strict-aliasing here

C++严格的混叠是木腿上的一个补丁.C++没有任何混淆信息,并且没有混淆信息防止了一些优化(如您在这里所说),因此为了恢复某些性能,严格的混叠被修补在…

不幸的是,在一种系统语言中,严格的别名是很尴尬的,因为重新解释原始内存是系统语言设计的本质.

更不幸的是,它没有实现那么多优化.例如,从一个数组复制到另一个数组时,必须假定数组可能重叠.

restrict(来自C)更有帮助,尽管它一次只适用于一个级别.


Instead, we have scope-based aliasing analysis

Rust中混叠分析的本质是基于lexical scopes(限制螺纹).

你可能知道的入门级解释是:

  • 如果你有一个&T,那么同一个实例没有&mut T
  • 如果你有一个&mut T,那么同一个实例就没有&T&mut T.

这是一个略为缩写的版本,适合初学者.例如:

fn main() {
    let mut i = 32;
    let mut_ref = &mut i;
    let x: &i32 = mut_ref;

    println!("{}", x);
}

很好,尽管&mut i32(mut_ref)和&i32(x)都指向同一个实例!

然而,如果你在形成x后try 进入mut_ref,真相就会揭晓:

fn main() {
    let mut i = 32;
    let mut_ref = &mut i;
    let x: &i32 = mut_ref;
    *mut_ref = 2;
    println!("{}", x);
}
error[E0506]: cannot assign to `*mut_ref` because it is borrowed
  |
4 |         let x: &i32 = mut_ref;
  |                       ------- borrow of `*mut_ref` occurs here
5 |         *mut_ref = 2;
  |         ^^^^^^^^^^^^ assignment to borrowed `*mut_ref` occurs here

因此,使&mut T&T同时指向同一存储器位置是fine;然而,只要&T存在,通过&mut T的变异就会被禁用.

从某种意义上说,&mut Ttemporarily降级为&T.


So, what of pointers?

首先,让我们回顾一下the reference:

  • 不保证指向有效内存,甚至不保证非空(与Box&不同);
  • Box不同,没有任何自动清理功能,因此需要手动资源管理;
  • 是普通的旧数据,也就是说,它们不会移动所有权,这与Box不同,因此Rust编译器无法防止免费使用之类的错误;
  • 不像&,没有任何形式的生命周期,因此编译器无法对悬空指针进行推理;和
  • 除了不允许直接通过*const T进行变异外,对别名或可变性没有任何保证.

显然,没有任何规则禁止投*const T*mut T.这是正常的,it's allowed,因此最后一点实际上更像是lint,因为它很容易处理.

Nomicon

如果没有pointing to the Nomicon,关于不安全 rust 蚀的讨论就不完整.

本质上,不安全Rust 的规则相当简单:坚持编译器在安全Rust 的情况下所能提供的任何保证.

这并没有像可能的那样有帮助,因为这些规则还没有固定下来;很抱歉

Then, what are the semantics for dereferencing raw pointers?

As far as I know1:

  • 如果从原始指针(&T&mut T)形成引用,则必须确保这些引用遵守的别名规则得到支持,
  • 如果你立即读/写,这会暂时形成一个参考.

也就是说,如果调用方对该位置具有可变访问权:

pub unsafe fn run_ptr_direct(a: *const i32, b: *mut f32) -> (i32, i32) {
    let x = *a;
    *b = 1.0;
    let y = *a;
    (x, y)
}

应该是有效的,因为*a的类型是i32,所以引用中没有生命周期的重叠.

然而,我希望:

pub unsafe fn run_ptr_modified(a: *const i32, b: *mut f32) -> (i32, i32) {
    let x = &*a;
    *b = 1.0;
    let y = *a;
    (*x, y)
}

这是未定义的行为,因为x将处于活动状态,而*b用于修改其内存.

注意这个变化是多么微妙.在unsafe个代码中很容易分解不变量.

1 And I might be wrong right now, or I may become wrong in the future

Rust相关问答推荐

为什么实例化核心::time::ns不安全?

捕获FnMut闭包的时间不够长

返回Result<;(),框<;dyn错误>>;工作

链表堆栈溢出

详尽的匹配模式绑定

如何处理闭包中的生命周期以及作为参数和返回类型的闭包?

Rust FFI 和 CUDA C 性能差异

Rust 中指向自身的引用如何工作?

如何在Rust中使用Serde创建一个自定义的反序列化器来处理带有内部标记的枚举

Rust编译器通过哪些规则来确保锁被释放?

为什么不能在 Rust 中声明静态或常量 std::path::Path 对象?

为什么不可变特征的实现可以是可变的?

如何判断服务器是否正确接收数据

以 `static` 为前缀的闭包是什么意思?我什么时候使用它?

如何在 Rust 中将 UTF-8 十六进制值转换为 char?

为什么在 macOS / iOS 上切换 WiFi 网络时 reqwest 响应会挂起?

为什么 &i32 可以与 Rust 中的 &&i32 进行比较?

使用 HashMap 条目时如何避免字符串键的短暂克隆?

从 Cranelift 发出 ASM

在 Rust 中组合特征的不同方法是否等效?