簡體   English   中英

解除引用原始指針的語義是什么?

[英]What are the semantics for dereferencing raw pointers?

對於共享引用和可變引用,語義是明確的:只要您具有對值的共享引用,其他任何內容都不能具有可變訪問權限,並且不能共享可變引用。

所以這段代碼:

#[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點是只讀一次,因為編譯器知道在寫b不能在修改了內存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

雖然我們寫了不同類型的值,但第二次讀取仍然轉到內存 - 似乎允許為兩個參數調用具有相同(或重疊)內存位置的函數。 換句話說, const原始指針不禁止共存的mut原始指針; 並且可能沒有兩個mut原始指針(可能是不同類型)到同一個(或重疊)的內存位置。

請注意,正常的優化C / C ++ - 編譯器將消除第二次讀取(由於“嚴格別名”規則:在大多數情況下,通過不同(“不兼容”)類型的指針修改/讀取相同的內存位置是UB):

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

帶有Rust代碼示例的游樂場

帶有C示例的godbolt Compiler Explorer

那么:如果我們直接使用原始指針,那么語義是什么:引用數據是否可以重疊?

這應該直接影響是否允許編譯器通過原始指針重新排序內存訪問。

這里沒有尷尬的嚴格別名

C ++ strict-aliasing是一條木腿上的補丁。 C ++沒有任何別名信息,並且沒有別名信息會阻止大量優化(正如您在此處所述),因此重新獲得某些性能嚴格別名已修補...

不幸的是,嚴格別名在系統語言中很尷尬,因為重新解釋原始內存是系統語言設計要做的本質。

而且不幸的是,它無法實現許多優化。 例如,從一個數組復制到另一個數組必須假定數組可能重疊。

restrict (來自C)稍微有點幫助,雖然它一次只適用於一個級別。


相反,我們有基於范圍的別名分析

Rust中別名分析的本質是基於詞法范圍 (限制線程)。

你可能知道的初級水平解釋是:

  • 如果你有一個&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 i32mut_ref )和一個&i32x )指向同一個實例,但是完全沒問題!

但是,如果你在形成x之后嘗試訪問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指向在同一時間同一個內存位置; 然而,只要&T存在,通過&mut T變異將被禁用。

從某種意義上說, &mut T 暫時降級為&T


那么,指針是什么?

首先,讓我們回顧一下參考文獻

  • 不保證指向有效內存,甚至不保證非NULL(與Box& )不同;
  • Box不同,沒有任何自動清理,因此需要手動資源管理;
  • 是純舊的數據,也就是說,它們不會移動所有權,與Box不同,因此Rust編譯器無法防止像免費使用后的錯誤;
  • 缺乏任何形式的生命周期,不像& ,因此編譯器無法推理懸掛指針;
  • 除了不允許直接通過*const T突變外,不保證別名或可變性。

明顯缺席的是禁止將*const T轉換為*mut T任何規則。 這是正常的, 這是允許的 ,因此最后一點實際上更像是一個棉絨 ,因為它可以很容易地解決。

Nomicon

如果沒有指向Nomicon,對不安全的Rust的討論就不會完整。

從本質上講,不安全Rust的規則相當簡單:如果它是安全的Rust,那么支持編譯器可以保證的任何保證。

這並沒有那么有用,因為這些規則尚未確定; 抱歉。

那么,解除引用原始指針的語義是什么?

據我所知 1

  • 如果你從原始指針( &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 我現在可能錯了,或者將來可能會出錯

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM