[英]Is there a clearer way of representing dereferencing raw pointers and applying their functions in Rust?
[英]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
帶有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 i32
( mut_ref
)和一個&i32
( x
)指向同一個實例,但是完全沒問題!
但是,如果你在形成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.