Rust: Masalah pelacakan untuk RFC 1892, "Hentikan tidak diinisialisasi untuk mendukung jenis MaybeUninit baru"

Dibuat pada 19 Agu 2018  ·  382Komentar  ·  Sumber: rust-lang/rust

MASALAH PELACAKAN BARU = https://github.com/rust-lang/rust/issues/63566

Ini adalah masalah pelacakan untuk RFC "Singkirkan uninitialized untuk mendukung jenis baru MaybeUninit " (rust-lang / rfcs # 1892).

Langkah:

  • [x] Terapkan RFC (cc @ rust-lang / libs)
  • [x] Sesuaikan dokumentasi (di https://github.com/rust-lang/rust/pull/60445)
  • [x] PR Stabilisasi (di https://github.com/rust-lang/rust/pull/60445)

Pertanyaan yang belum terselesaikan:

  • Haruskah kita memiliki penyetel aman yang mengembalikan &mut T ?
  • Haruskah kita mengganti nama MaybeUninit ?
  • Haruskah kita mengganti nama into_inner ?
  • Haruskah MaybeUninit<T> menjadi Copy untuk T: Copy ?
  • Haruskah kita mengizinkan panggilan get_ref dan get_mut (tetapi tidak membaca dari referensi yang dikembalikan) sebelum data diinisialisasi? (AKA: "Apakah referensi ke data yang tidak diinisialisasi insta-UB, atau hanya UB saat dibaca?") Haruskah kita mengganti namanya mirip dengan into_inner ?
  • Bisakah kita membuat into_inner (atau apa pun namanya) panik ketika T tidak berpenghuni, seperti mem::uninitialized sekarang? (selesai)
  • Sepertinya kami ingin tidak menghentikan mem::zeroed .
B-RFC-approved C-tracking-issue E-mentor T-lang T-libs

Komentar yang paling membantu

mem::zeroed() berguna untuk kasus FFI tertentu di mana Anda diharapkan nol nilai dengan memset(&x, 0, sizeof(x)) sebelum memanggil fungsi C. Saya pikir ini adalah alasan yang cukup untuk membuatnya tidak digunakan lagi.

Semua 382 komentar

cc @RalfJung

[] Terapkan RFC

Saya dapat membantu menerapkan RFC.

Luar biasa, saya dapat membantu meninjau :)

Saya ingin beberapa klarifikasi tentang bagian RFC ini:

Membuat panggilan tidak diinisialisasi pada jenis kosong memicu kepanikan waktu proses yang juga mencetak pesan penghentian.

Haruskah hanya mem::uninitialized::<!>() panik? Atau haruskah ini juga mencakup struct (dan mungkin enums?) Yang berisi tipe kosong (misalnya (!, u8) )?

AFAIK kami hanya melakukan pembuatan kode yang benar-benar berbahaya untuk ! . Kebanyakan penggunaan lain dari mem::uninitialized sama salahnya, tetapi kompilator tidak mengeksploitasinya.

Jadi saya akan melakukannya hanya dengan ! , tetapi juga untuk mem::zeroed . (Sepertinya saya lupa mengubah bagian itu ketika saya menambahkan zeroed ke RFC.)

Kita bisa memulai dengan membuat ini:
https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/intrinsic.rs#L184 -L198

periksa apakah fn_ty.ret.layout.abi adalah Abi::Uninhabited dan setidaknya mengeluarkan jebakan, misalnya: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/ operand.rs # L400 -L403

Setelah Anda melihat jebakan (yaitu intrinsics::abort ) beraksi, Anda dapat melihat apakah ada cara yang bagus untuk memicu kepanikan. Ini rumit karena melepasnya, kita perlu kasus khusus di sini: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L445 - L447

Untuk benar-benar panik, Anda memerlukan sesuatu seperti ini: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L360 -L407
(Anda dapat mengabaikan lengan EvalErrorKind::BoundsCheck )

@eddyb Terima kasih atas petunjuknya.


Saya sekarang memperbaiki (beberapa) peringatan penghentian dan saya merasa (sangat) tergoda untuk menjalankan sed -i s/mem::uninitialized()/mem::MaybeUninit::uninitialized().into_inner()/g tapi saya rasa itu akan meleset ... Atau apakah itu OK jika saya tahu bahwa nilainya adalah konkret (Salin) tipe? misalnya let x: [u8; 1024] = mem::uninitialized(); .

Itu akan kehilangan intinya, ya. ^^

Setidaknya untuk saat ini, saya ingin mempertimbangkan mem::MaybeUninit::uninitialized().into_inner() UB untuk semua tipe non-serikat. Perhatikan bahwa Copy tentu saja tidak cukup; keduanya bool dan &'static i32 adalah Copy dan potongan Anda dimaksudkan untuk menjadi insta-UB bagi mereka. Kita mungkin menginginkan pengecualian untuk "tipe di mana semua pola bit baik-baik saja" (tipe integer, pada dasarnya), tetapi saya akan menentang membuat pengecualian seperti itu karena undef bukanlah pola bit normal. Itulah mengapa RFC mengatakan Anda perlu menginisialisasi sepenuhnya sebelum memanggil into_inner .

Ia juga mengatakan bahwa untuk get_mut , tetapi diskusi RFC yang diajukan oleh beberapa orang untuk melonggarkan pembatasan di sini. Itu adalah opsi yang bisa saya jalani. Tapi tidak untuk into_inner .

Saya khawatir semua penggunaan uninitialized ini harus ditinjau lebih hati-hati, dan sebenarnya ini adalah salah satu tujuan RFC. Kami ingin ekosistem yang lebih luas lebih berhati-hati di sini, jika semua orang segera menggunakan into_inner maka RFC tidak berharga.

Kami ingin ekosistem yang lebih luas lebih berhati-hati di sini, jika semua orang langsung menggunakan into_inner maka RFC tidak ada gunanya.

Ini memberi saya ide ... mungkin kita harus lint (grup: "kebenaran") untuk kode semacam ini? cc @ oli-obk

Saya sekarang memperbaiki (beberapa) peringatan penghentian

Kami hanya harus mengirimkan Nightly dengan peringatan tersebut setelah penggantian yang direkomendasikan tersedia setidaknya di Stable. Lihat diskusi serupa di https://github.com/rust-lang/rust/pull/52994#issuecomment -411413493

@Ralfian

Kami mungkin menginginkan pengecualian untuk "jenis yang semua pola bitnya baik-baik saja" (jenis integer, pada dasarnya)

Anda telah berpartisipasi dalam diskusi tentang ini sebelumnya, tetapi saya akan memposting di sini untuk diedarkan lebih luas: ini sudah sesuatu yang kami memiliki banyak kasus penggunaan yang ada di Fuchsia, dan kami memiliki sifat untuk ini ( FromBytes ) dan makro turunan untuk jenis ini. Ada juga Pre-RFC internal untuk menambahkan ini ke perpustakaan standar (cc @gnzlbg @joshlf).

Saya akan menentang membuat pengecualian seperti itu karena undef bukanlah pola bit normal.

Ya, ini adalah aspek di mana mem::zeroed() sangat berbeda dari mem::uninitialized() .

@rumahsakitotak

Anda telah berpartisipasi dalam diskusi tentang ini sebelumnya, tetapi saya akan memposting di sini untuk diedarkan lebih luas: ini sudah sesuatu yang kami memiliki banyak kasus penggunaan yang ada di Fuchsia, dan kami memiliki sifat untuk ini (FromBytes) dan makro turunan untuk tipe ini. Ada juga Pre-RFC internal untuk menambahkan ini ke perpustakaan standar (cc @gnzlbg @joshlf).

Diskusi tersebut adalah tentang cara-cara untuk mengizinkan memcpy s yang aman di semua jenis, tetapi saya pikir itu cukup ortogonal apakah memori yang sedang disalin diinisialisasi atau tidak - jika Anda memasukkan memori yang tidak diinisialisasi, Anda mendapatkan memori yang tidak diinisialisasi.

Konsensus juga adalah bahwa tidak masuk akal untuk setiap pendekatan yang didiskusikan untuk memungkinkan membaca padding byte, yang merupakan bentuk memori yang tidak diinisialisasi, di Rust yang aman. Itu jika Anda memasukkan memori yang diinisialisasi, Anda tidak bisa mengeluarkan memori yang tidak diinisialisasi.

IIRC, tidak ada orang di sana yang menyarankan atau membahas pendekatan apa pun di mana Anda dapat memasukkan memori yang tidak diinisialisasi dan mendapatkan memori yang diinisialisasi, jadi saya tidak mengikuti apa yang harus dilakukan diskusi tersebut dengan yang satu ini. Bagi saya mereka sepenuhnya ortogonal.

Untuk mengarahkan intinya sedikit lebih jauh, LLVM mendefinisikan data yang tidak diinisialisasi sebagai Poison, yang berbeda dari "beberapa pola bit yang sewenang-wenang tetapi valid." Percabangan berdasarkan nilai Poison atau menggunakannya untuk menghitung alamat yang kemudian direferensikan adalah UB. Jadi, sayangnya, "tipe yang semua pola bitnya baik-baik saja" masih tidak aman untuk dibuat karena menggunakannya tanpa menginisialisasi secara terpisah akan menjadi UB.

Benar, maaf, saya seharusnya menjelaskan apa yang saya maksud. Saya mencoba mengatakan bahwa "jenis yang semua pola bitnya baik-baik saja" sudah menjadi sesuatu yang ingin kami definisikan karena alasan lain. Seperti yang dikatakan @RalfJung di atas,

Saya akan menentang membuat pengecualian seperti itu karena undef bukanlah pola bit normal.

Alhamdulillah ada orang yang bisa membaca, karena ternyata saya tidak bisa ...

Benar, jadi yang ingin saya katakan adalah: Kami pasti memiliki tipe di mana semua pola bit yang diinisialisasi baik-baik saja - semua tipe i* dan u* , petunjuk mentah, menurut saya f* serta dan kemudian tupel / struct hanya terdiri dari tipe seperti itu.

Apa yang menjadi pertanyaan terbuka adalah dalam keadaan apa dari jenis ini yang diizinkan untuk tidak diinisialisasi , yaitu racun. Jawaban pilihan saya sendiri adalah "tidak pernah".

Konsensus juga adalah bahwa tidak masuk akal untuk setiap pendekatan yang didiskusikan untuk memungkinkan membaca padding byte, yang merupakan bentuk memori yang tidak diinisialisasi, di Rust yang aman. Itu jika Anda memasukkan memori yang diinisialisasi, Anda tidak bisa mengeluarkan memori yang tidak diinisialisasi.

Membaca byte padding sebagai MaybeUninit<u8> seharusnya baik-baik saja.

Konsensus juga adalah bahwa tidak masuk akal untuk setiap pendekatan yang didiskusikan untuk memungkinkan membaca padding byte, yang merupakan bentuk memori yang tidak diinisialisasi, di Rust yang aman. Itu jika Anda memasukkan memori yang diinisialisasi, Anda tidak bisa mengeluarkan memori yang tidak diinisialisasi.

Membaca byte padding sebagai MaybeUninitseharusnya baik-baik saja.

Diskusi singkatnya adalah tentang menyediakan sebuah ciri, Compatible<T> , dengan metode yang aman fn safe_transmute(self) -> T yang "menafsirkan ulang" / "memcpys" bit self menjadi T . Jaminan dari metode ini adalah jika self diinisialisasi dengan benar, begitu juga dengan hasil T . Diusulkan agar kompilator mengisi implementasi transitif secara otomatis, misalnya, jika ada impl Compatible<V> for U , dan impl Compatible<W> for V maka ada impl Compatible<W> for U (baik karena telah disediakan secara manual, atau kompilator otomatis membuatnya - bagaimana hal ini dapat diimplementasikan sepenuhnya dilakukan dengan tangan).

Diusulkan bahwa seharusnya unsafe untuk mengimplementasikan sifat: jika Anda menerapkannya untuk T yang memiliki padding byte di mana Self memiliki bidang, maka semuanya baik-baik saja setidaknya sampai Anda mencoba menggunakan T dan perilaku program Anda tergantung pada isi dari memori yang tidak diinisialisasi.

Saya tidak tahu apa hubungannya semua ini dengan MaybeUninit<u8> , mungkin Anda bisa menguraikannya?

Satu-satunya hal yang dapat saya bayangkan adalah bahwa kita dapat menambahkan implan selimut: unsafe impl<T> Compatible<[MaybeUninit<u8>; size_of::<T>()]> for T { ... } karena mengubah jenis apa pun menjadi [MaybeUninit<u8>; N] ukurannya aman untuk semua jenis. Saya tidak tahu seberapa berguna impl semacam itu, mengingat bahwa MaybeUninit adalah gabungan, dan siapa pun yang menggunakan [MaybeUninit<u8>; N] tidak tahu apakah elemen tertentu dari array diinisialisasi atau tidak .

@gnzlbg saat itu Anda berbicara tentang FromBits<T> for [u8] . Di situlah saya mengatakan kita harus menggunakan [MaybeUninit<u8>] sebagai gantinya.

Saya membahas proposal ini dengan @nikomatsakis di RustConf, dan dia mendorong saya untuk melanjutkan RFC. Saya akan melakukannya dalam beberapa minggu, tetapi jika ada minat, saya bisa mencoba menyelesaikannya akhir pekan ini. Apakah itu berguna untuk diskusi ini?

@ Joshlf proposal mana yang kamu bicarakan?

@Ralfian

@gnzlbg saat itu Anda membicarakan FromBitsuntuk [u8]. Di situlah saya mengatakan kita harus menggunakan [MaybeUninit] sebagai gantinya.

Gotcha, setuju sepenuhnya di sini. Benar-benar lupa bahwa kami juga ingin melakukan itu 😆

@ Joshlf proposal mana yang kamu bicarakan?

Sebuah proposal FromBits / IntoBits . TLDR: T: FromBits<U> berarti bahwa pola bit apa pun yang valid U sesuai dengan T valid. U: IntoBits<T> berarti hal yang sama. Kompiler secara otomatis menyimpulkan keduanya untuk semua pasangan tipe yang diberikan aturan tertentu, dan ini membuka banyak kebaikan yang saat ini membutuhkan unsafe . Ada draf RFC ini di sini yang saya tulis beberapa waktu yang lalu, tetapi saya bermaksud untuk mengubah sebagian besar darinya, jadi jangan menganggap teks itu sebagai sesuatu yang lebih dari panduan kasar.

@ Joshlf Saya pikir sepasang sifat seperti itu akan lebih berkembang di atas diskusi ini daripada menjadi bagian darinya. AFAIK kami memiliki dua pertanyaan terbuka dalam hal validitas:

  • Apakah itu berulang di bawah referensi? Saya semakin yakin seharusnya tidak, karena kita melihat lebih banyak contoh. Jadi sepertinya kita harus menyesuaikan MaybeUninit::get_mut docs sesuai (sebenarnya bukan UB yang menggunakannya sebelum menyelesaikan inisialisasi, tetapi UB yang membedakannya sebelum menyelesaikan inisialisasi). Namun, pertama-tama kami harus membuat keputusan itu untuk validitas, dan saya tidak yakin tempat yang tepat untuk itu. Mungkin RFC khusus?
  • Apakah u8 (dan tipe integer lainnya, floating point, raw pointer) harus diinisialisasi, yaitu MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Saya rasa begitu, tetapi sebagian besar berdasarkan firasat bahwa kami ingin mempertahankan tempat di mana kami mengizinkan minimal poison / undef . Namun, saya bisa diyakinkan sebaliknya jika ada banyak kegunaan dari pola ini (dan saya berharap bisa menggunakan miri untuk membantu menentukan ini).

Apakah itu berulang di bawah referensi?

@RalfJung Dapatkah Anda menunjukkan contoh apa yang Anda maksud dengan "berulang di bawah referensi"?

Apakah u8 (dan tipe integer lainnya, floating point, raw pointer) harus diinisialisasi, yaitu MaybeUinit:: uninitialized (). into_inner () insta-UB?

Apa jadinya jika UB tidak instan? Apa yang dapat saya lakukan dengan nilai itu? Bisakah saya mencocokkannya? Jika ya, apakah perilaku program bersifat deterministik?

Saya merasa jika saya tidak dapat mencocokkan nilainya tanpa memperkenalkan UB, maka kami telah menemukan kembali mem::uninitialized . Jika saya dapat mencocokkan nilai dan cabang yang sama selalu diambil di semua arsitektur, tingkat-opt, dll. Kami telah menemukan kembali mem::zeroed (dan semacam memanfaatkan MaybeUninit ketik sedikit diperdebatkan). Jika perilaku program tidak deterministik, dan berubah dengan tingkat pengoptimalan, di seluruh arsitektur, bergantung pada faktor eksternal (seperti apakah OS memberikan proses pada halaman nol), dll., Maka saya merasa seperti kami akan memperkenalkan senjata besar ke dalam bahasa.

Apakah u8 (dan tipe integer lainnya, floating point, raw pointer) harus diinisialisasi, yaitu MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Saya rasa begitu, tetapi sebagian besar berdasarkan firasat bahwa kami ingin mempertahankan tempat di mana kami mengizinkan minimal poison / undef . Namun, saya bisa diyakinkan sebaliknya jika ada banyak kegunaan dari pola ini (dan saya berharap bisa menggunakan miri untuk membantu menentukan ini).

FWIW, dua keuntungan dari ini tidak menjadi UB adalah bahwa a) itu sejalan dengan apa yang LLVM lakukan dan, b) memungkinkan lebih banyak fleksibilitas dalam optimasi. Ini juga tampak lebih konsisten dengan proposal Anda baru-baru ini untuk mendefinisikan keselamatan pada waktu penggunaan, bukan pada waktu konstruksi.

Apa jadinya jika UB tidak instan? Apa yang dapat saya lakukan dengan nilai itu? Bisakah saya mencocokkannya? Jika ya, apakah perilaku program bersifat deterministik?

Saya merasa jika saya tidak dapat mencocokkan nilainya tanpa memperkenalkan UB, maka kami telah menemukan kembali mem::uninitialized . Jika saya dapat mencocokkan nilai dan cabang yang sama selalu diambil di semua arsitektur, level-opt, dll. Kami telah menemukan kembali mem::zeroed (dan semacam memanfaatkan MaybeUninit ketik sedikit diperdebatkan). Jika perilaku program tidak deterministik, dan berubah dengan tingkat pengoptimalan, di seluruh arsitektur, bergantung pada faktor eksternal (seperti apakah OS memberikan proses pada halaman nol), dll., Maka saya merasa seperti kami akan memperkenalkan senjata besar ke dalam bahasa.

Mengapa Anda ingin dapat mencocokkan sesuatu yang belum diinisialisasi? Mendefinisikan sebagai UB untuk bercabang atau mengindeks berdasarkan nilai yang tidak diinisialisasi memberi LLVM lebih banyak ruang untuk dioptimalkan, jadi menurut saya tidak lebih banyak mengikat tangan adalah ide yang baik, terutama jika tidak ada kasus penggunaan yang menarik.

Mengapa Anda ingin dapat mencocokkan sesuatu yang belum diinisialisasi?

Saya tidak mengatakan saya ingin, saya menyatakan bahwa jika ini tidak dapat dilakukan, saya tidak mengerti perbedaan antara MaybeUinit<u8>::uninitialized().into_inner() dan hanya mem::uninitialized() .

@RalfJung Dapatkah Anda menunjukkan contoh apa yang Anda maksud dengan "berulang di bawah referensi"?

Pada dasarnya, pertanyaannya adalah apakah kita mengizinkan hal berikut:

let mut b = MaybeUninit::<bool>::uninitialized();
let bref = b.get_mut(); // insta-UB?

Jika kita memutuskan bahwa referensi hanya valid jika menunjuk ke sesuatu yang valid (itulah yang saya maksud dengan "berulang di bawah referensi"), kode ini adalah UB.

Apa jadinya jika UB tidak instan? Apa yang dapat saya lakukan dengan nilai itu? Bisakah saya mencocokkannya? Jika ya, apakah perilaku program bersifat deterministik?

Anda tidak dapat memeriksa u8 tidak diinisialisasi dengan cara apa pun. match dapat melakukan banyak hal, baik mengikat nama dan benar-benar menguji kesetaraan; yang pertama tidak apa-apa tetapi yang terakhir tidak. Tapi Anda bisa menuliskannya kembali ke memori.

Intinya, inilah yang miri terapkan saat ini.

Saya merasa jika saya tidak dapat mencocokkan nilai tanpa memperkenalkan UB, maka kami telah menemukan kembali mem :: uninitialized.

Mengapa demikian? Masalah terbesar dengan mem::uninitialized adalah seputar tipe yang memiliki batasan untuk nilai validnya. Kami dapat memutuskan bahwa u8 tidak memiliki batasan seperti itu, jadi mem::uninitialized() tidak masalah untuk u8 . Itu hampir tidak mungkin untuk digunakan dengan benar dalam kode generik, jadi lebih baik untuk menghilangkannya sepenuhnya.
Apa pun pilihannya, meneruskan u8 tidak diinisialisasi ke kode yang aman masih tidak diperbolehkan, tetapi mungkin tidak masalah untuk menggunakannya dengan hati-hati dalam kode yang tidak aman.

Anda juga tidak dapat "mencocokkan" pada &mut mengarah ke data yang tidak valid. IOW, saya pikir contoh bool saya berikan di atas baik-baik saja, tetapi yang berikut ini jelas tidak:

let mut b = MaybeUninit::<bool>::uninitialized();
let bref = b.get_mut();
match bref {
  &b => // insta-UB! We have a bad bool in scope.
}

Ini menggunakan match untuk melakukan dereferensi penunjuk normal.

FWIW, dua keuntungan dari ini tidak menjadi UB adalah bahwa a) itu sejalan dengan apa yang LLVM lakukan dan, b) memungkinkan lebih banyak fleksibilitas dalam optimasi. Ini juga tampak lebih konsisten dengan proposal Anda baru-baru ini untuk mendefinisikan keselamatan pada waktu penggunaan, bukan pada waktu konstruksi.

Pengoptimalan mana yang memungkinkan?
Perhatikan bahwa LLVM melakukan pengoptimalan pada kode yang pada dasarnya tidak diketik, jadi tidak ada yang menjadi perhatian di sana. Kami hanya berbicara tentang pengoptimalan MIR di sini.

Saya pada dasarnya datang dari perspektif yang harus kita izinkan sesedikit mungkin sampai kita memiliki penggunaan yang jelas. Kami selalu dapat mengizinkan lebih banyak barang nanti, tetapi tidak sebaliknya. Yang mengatakan, beberapa penggunaan yang baik dari potongan byte yang bisa menjadi data lama apapun telah muncul baru-baru ini, yang mungkin cukup argumen untuk melakukan ini setidaknya untuk u* dan i* .

Jika kita memutuskan bahwa referensi hanya valid jika menunjuk ke sesuatu yang valid (itulah yang saya maksud dengan "berulang di bawah referensi"), kode ini adalah UB.

Kena kau.

Masalah terbesar dengan mem :: uninitialized adalah seputar tipe yang memiliki batasan untuk nilai validnya.

mem::uninitialized juga memiliki masalah yang Anda tunjukkan di atas: bahwa membuat referensi ke nilai yang tidak diinisialisasi mungkin merupakan perilaku yang tidak terdefinisi (atau tidak). Jadi apakah UB berikut ini?

let mut b = MaybeUninit::<u8>::uninitialized().into_inner();
let bref = &mut b; // Insta UB ?

Saya pikir salah satu alasan memperkenalkan MaybeUninit adalah untuk menghindari masalah ini dengan selalu menginisialisasi serikat (misalnya ke unit), yang memungkinkan Anda untuk mengambil referensi ke sana, dan mengubah isinya, dengan misalnya menyetel bidang aktif ke u8 dan memberinya nilai melalui ptr::write tanpa memperkenalkan UB.

Jadi inilah kenapa saya agak bingung. Saya tidak melihat bagaimana into_inner lebih baik dari:

let mut b: u8 = uninitialized();
let bref = &mut b; // Insta UB ? 

Keduanya tampak seperti bom waktu perilaku yang tidak terdefinisi bagi saya.

Pengoptimalan mana yang memungkinkan?
Perhatikan bahwa LLVM melakukan pengoptimalan pada kode yang pada dasarnya tidak diketik, jadi tidak ada yang menjadi perhatian di sana. Kami hanya berbicara tentang pengoptimalan MIR di sini.

Jika kami mengatakan bahwa memori tak terdefinisi memiliki beberapa nilai, dan dengan demikian Anda diizinkan untuk bercabang di atasnya sesuai dengan semantik Rust, maka kami tidak dapat menurunkannya ke versi tak terdefinisi LLVM, karena itu akan menjadi tidak sehat.

Saya pada dasarnya datang dari perspektif yang harus kita izinkan sesedikit mungkin sampai kita memiliki penggunaan yang jelas. Kami selalu dapat mengizinkan lebih banyak barang nanti, tetapi tidak sebaliknya.

Itu adil.

Meskipun demikian, beberapa penggunaan yang baik dari potongan byte yang dapat menjadi data lama telah muncul baru-baru ini, yang mungkin cukup sebagai argumen untuk melakukan ini setidaknya untuk u* dan i* .

Apakah salah satu dari kasus penggunaan ini termasuk memiliki potongan byte yang menyimpan nilai yang tidak diinisialisasi?

Satu tempat di mana nilai &mut [u8] yang tidak diinisialisasi-tetapi-tidak-racun adalah untuk Read::read - kami ingin menghindari keharusan untuk membekukan buffer hanya karena Read aneh

Satu tempat di mana nilai &mut [u8] yang tidak diinisialisasi-tetapi-tidak-racun adalah untuk Read::read - kami ingin menghindari keharusan untuk membekukan buffer hanya karena Read aneh

Begitu, jadi idenya adalah bahwa MaybeUninit akan mewakili jenis yang diinisialisasi, tetapi dengan konten yang tidak ditentukan, sementara jenis data yang tidak diinisialisasi (misalnya, bidang padding) masih akan sepenuhnya tidak diinisialisasi dalam pengertian racun LLVM?

Saya tidak berpikir itu perlu diterapkan ke MaybeUninit secara umum. Secara teori, mungkin ada beberapa API untuk "membekukan" konten dari tidak terdefinisi menjadi ditentukan-tetapi-sewenang-wenang.

Jika kami mengatakan bahwa memori tak terdefinisi memiliki beberapa nilai, dan dengan demikian Anda diizinkan untuk bercabang di atasnya sesuai dengan semantik Rust, maka kami tidak dapat menurunkannya ke versi tak terdefinisi LLVM, karena itu akan menjadi tidak sehat.

Itu tidak pernah menjadi proposal. Ini adalah dan akan tetap menjadi UB untuk bercabang di poison .

Pertanyaannya adalah apakah UB hanya "memiliki" poison dalam u8 .

Apakah salah satu dari kasus penggunaan ini termasuk memiliki potongan byte yang menyimpan nilai yang tidak diinisialisasi?

Irisan seperti referensi, jadi &mut [u8] data yang tidak diinisialisasi baik-baik saja selama itu hanya ditulis ke dalamnya (dengan asumsi itu adalah solusi yang kami ambil untuk validitas referensi).

@tokopedia

Satu tempat di mana sebuah uninitialized-but-not-poison & mut [u8] dapat berharga adalah untuk Read :: read - kami ingin dapat menghindari keharusan ke nol buffer hanya karena beberapa impl Read yang aneh dapat membaca darinya daripada hanya menuliskannya.

Nah, tanpa &out Anda hanya akan bisa melakukan itu jika Anda tahu impl. Pertanyaannya bukanlah apakah kode aman harus menangani poison dalam u8 (tidak, itu bukan penggunaan yang baik dari kode aman!), Pertanyaannya adalah apakah kode yang tidak aman dapat menanganinya dengan hati

Mungkin saya terlambat, tapi saya sarankan untuk mengubah tanda tangan dari set() metode untuk mengembalikan &mut T . Dengan cara ini, akan aman untuk menulis kode yang sepenuhnya aman bekerja dengan MaybeUninit (setidaknya dalam beberapa situasi).

fn init(dest: &mut MaybeUninit<u8>) -> &mut u8 {
    dest.set(produce_value())
}

Ini praktis merupakan jaminan statis bahwa init() akan menginisialisasi nilai atau menyimpang. (Jika mencoba mengembalikan sesuatu yang lain, masa hidup akan salah dan &'static mut u8 tidak mungkin dalam kode aman.) Mungkin itu bisa digunakan sebagai bagian dari API placer di masa depan.

@Kixunil Sudah seperti itu sebelumnya, dan saya setuju itu bagus. Saya baru saja menemukan set membingungkan untuk fungsi yang mengembalikan sesuatu.

@Bayu_joo

Ini praktis merupakan jaminan statis bahwa init() akan menginisialisasi nilai atau menyimpang. (Jika mencoba mengembalikan sesuatu yang lain, seumur hidup akan salah dan &'static mut u8 tidak mungkin dalam kode aman.)

Tidak terlalu; Anda bisa mendapatkannya dengan Box::leak .

Dalam basis kode yang saya tulis baru-baru ini, saya menemukan skema serupa; ini sedikit lebih rumit, tetapi memberikan jaminan statis yang sebenarnya bahwa referensi yang diberikan telah diinisialisasi. Dari pada

fn init(dest: &mut MaybeUninit<u8>) -> &mut u8

saya sudah

fn init<'a>(dest: Uninitialized<'a, u8>) -> DidInit<'a, u8>

Triknya adalah bahwa Uninitialized dan DidInit keduanya tidak berubah pada parameter masa pakainya, jadi tidak ada cara untuk menggunakan kembali DidInit dengan parameter masa hidup yang berbeda, bahkan misalnya 'static .

DidInit impls Deref dan DerefMut , jadi kode aman dapat menggunakannya sebagai referensi, seperti dalam contoh Anda. Namun jaminan bahwa sebenarnya referensi asli yang diteruskan yang diinisialisasi, bukan referensi acak lainnya, berguna untuk kode yang tidak aman . Artinya Anda dapat mendefinisikan penginisialisasi secara struktural:

struct Foo {
    a: i32,
    b: u8,
}

fn init_foo<'a>(dest: Uninitialized<'a, Foo>,
                init_a: impl for<'x> FnOnce(Uninitialized<'x, i32>) -> DidInit<'x, i32>,
                init_b: impl for<'x> FnOnce(Uninitialized<'x, u8>) -> DidInit<'x, u8>)
                -> &'a mut DidInit<'a, Foo> {
    let ptr: *mut Foo = dest.ptr;
    unsafe {
        init_a(Uninitialized::new(&mut (*ptr).a));
        init_b(Uninitialized::new(&mut (*ptr).b));
        dest.did_init()
    }
}

Fungsi ini menginisialisasi pointer ke struct Foo dengan menginisialisasi setiap bidangnya secara bergantian, menggunakan callback inisialisasi yang disediakan pengguna. Ini mengharuskan callback mengembalikan DidInit s, tetapi tidak peduli tentang nilainya; fakta bahwa mereka ada sudah cukup. Setelah semua bidang diinisialisasi, ia mengetahui bahwa seluruh Foo valid - sehingga memanggil did_init() pada Uninitialized<'a, Foo> , yang merupakan metode tidak aman yang hanya memasukkannya ke sesuai DidInit type, yang kemudian dikembalikan init_foo .

Saya juga memiliki makro yang mengotomatiskan proses penulisan fungsi seperti itu, dan versi aslinya sedikit lebih berhati-hati tentang destruktor dan kepanikan (meskipun perlu perbaikan).

Bagaimanapun, saya bertanya-tanya apakah hal seperti ini bisa diterapkan di perpustakaan standar.

Tautan taman bermain

(Catatan: DidInit<'a, T> sebenarnya adalah tipe alias untuk &'a mut _DidInitMarker<'a, T> , untuk menghindari masalah seumur hidup dengan DerefMut .)

Omong-omong, sementara pendekatan terkait di atas mengabaikan destruktor, pendekatan yang sedikit berbeda adalah membuat DidInit<‘a, T> bertanggung jawab untuk menjalankan destruktor T . Dalam hal ini harus berupa struct, bukan alias; dan hanya dapat membagikan referensi ke T yang hidup selama DidInit itu sendiri, bukan untuk semua ’a (karena jika tidak, Anda dapat terus mengaksesnya setelah penghancuran).

+1 untuk menyertakan metode untuk memberikan perilaku yang sebelumnya saya minta dalam set , tetapi saya baik-baik saja dengan itu tersedia melalui nama lain.

Ada ide bagus untuk nama apa itu? set_and_as_mut ? ^^

set_and_borrow_mut ?

insert / insert_mut ? Jenis Entry memiliki metode or_insert agak mirip (tetapi OccupiedEntry juga memiliki insert yang mengembalikan nilai lama, jadi itu sama sekali tidak mirip).

Apakah ada alasan yang sangat kuat untuk memiliki dua metode terpisah? Tampaknya cukup sederhana untuk mengabaikan nilai yang dikembalikan, dan saya membayangkan fungsinya akan ditandai sebagai #[inline] jadi saya tidak akan mengharapkan biaya runtime nyata.

Apakah ada alasan yang sangat kuat untuk memiliki dua metode terpisah? Tampaknya cukup sederhana untuk mengabaikan nilai pengembalian

Saya kira satu-satunya alasan adalah melihat set mengembalikan sesuatu yang agak mengejutkan.

Mungkin saya melewatkan sesuatu, tetapi apa yang dapat menyelamatkan kita dari memiliki nilai yang tidak valid? Maksud saya jika kita

let mut foo: MaybeUninit<T> = MaybeUninit {
    uninit: (),
};
let mut foo_ref = &mut foo as *mut MaybeUninit<T>;

unsafe {
    some_native_function(&mut (*foo_ref).value, val);
}

bagaimana jika some_native_function adalah no-op dan tidak benar-benar memasukkan nilainya? Apakah masih UB? Bagaimana cara menanganinya?

@Pzixel ini semua tercakup dalam dokumentasi API untuk MaybeUninit .

Jika some_native_function adalah NOP, tidak ada yang terjadi; jika Anda kemudian menggunakan foo_ref.value (atau lebih tepatnya melakukan foo_ref.as_mut() karena Anda hanya dapat menggunakan API publik), itu adalah UB karena fungsinya hanya dapat dipanggil setelah semuanya diinisialisasi.

MaybeUninit tidak mencegah memiliki nilai yang tidak valid - jika bisa, itu akan aman, tapi itu tidak mungkin. Namun, itu membuat bekerja dengan nilai yang tidak valid menjadi kurang dari sebuah footgun karena sekarang informasi bahwa nilainya mungkin tidak valid dikodekan dalam jenisnya, untuk dilihat oleh compiler dan programmer.

Saya ingin mendokumentasikan percakapan IRC yang saya lakukan dengan @sfackler mengenai masalah hipotetis yang dapat muncul di masa mendatang.

Pertanyaan utamanya adalah apakah mem::zeroed adalah representasi dalam memori yang valid untuk proposal implementasi saat ini untuk MaybeUninit<NonZeroU8> . Dalam pemikiran saya, dalam status "uninit" nilainya hanya padding, yang dapat digunakan kompilator untuk tujuan apa pun, dan dalam status "nilai", semua nilai yang mungkin kecuali mem::zeroed adalah valid (karena NonZero ).

Sistem tata letak tipe masa depan dengan pengemasan diskriminan enum yang lebih canggih (daripada yang kita miliki sekarang) mungkin kemudian menyimpan diskriminan dalam padding status "uninit" / memori nol dalam status "nilai". Dalam sistem hipotesis tersebut, ukuran Option<MaybeUninit<NonZeroU8>> adalah 1, sedangkan saat ini adalah 2. Selanjutnya, dalam sistem hipotesis tersebut, Some(MaybeUninit::uninitialized()) tidak dapat dibedakan dari None . Saya pikir kita mungkin dapat memperbaikinya dengan mengubah implementasi MaybeUninit (tetapi bukan API publiknya) setelah kita pindah ke sistem seperti itu.

Saya tidak melihat perbedaan antara NonZeroU8 dan &'static i32 dalam hal ini. Keduanya adalah tipe di mana "0" tidak valid . Jadi untuk keduanya, MaybeUninit<T>::zeroed().into_inner() adalah insta-UB.

Apakah Option<Union> dapat melakukan pengoptimalan layout tergantung pada validitas apa untuk sebuah gabungan. Ini belum diputuskan untuk semua kasus, tetapi ada kesepakatan umum bahwa untuk serikat yang memiliki varian tipe () , pola bit apa pun valid dan karenanya tidak ada pengoptimalan tata letak yang dimungkinkan. Ini mencakup MaybeUninit . Jadi Option<MaybeUninit<NonZeroU8>> tidak akan pernah memiliki ukuran 1.

ada kesepakatan umum bahwa untuk serikat yang memiliki varian type (), pola bit apa pun valid dan karenanya tidak ada pengoptimalan tata letak yang mungkin.

Apakah ini kasus khusus untuk "serikat pekerja yang memiliki varian type ()"? Apakah stabilisasi fitur ini secara implisit menstabilkan bagian Rust ABI? Bagaimana dengan union berisi struct UnitType; atau struct NewType(()); ? Bagaimana dengan struct Padded (di bawah)? Bagaimana dengan union berisi struct Padded ?

#[repr(C, align(4))]
struct Padded {
    a: NonZeroU8,
    b: (),
    c: NonZeroU16
}

Kata-kata saya sangat spesifik karena ini adalah satu-satunya hal yang saya cukup yakin kita memiliki kesepakatan umum. :) Saya pikir kami ingin membuat ini tergantung pada ukurannya saja (yaitu, semua ZST akan mendapatkan ini), tetapi sebenarnya saya pikir varian ini seharusnya tidak diperlukan dan serikat pekerja tidak akan pernah mendapatkan pengoptimalan tata letak secara default (tetapi pada akhirnya pengguna mungkin dapat ikut serta menggunakan atribut). Tapi itu hanya pendapat saya.

Kami akan melakukan diskusi yang tepat untuk mengukur konsensus saat ini dan mungkin mendapatkan kesepakatan tentang lebih banyak hal di salah satu diskusi berikutnya di repo UCG , dan Anda dipersilakan untuk bergabung di sana ketika itu terjadi.

Apakah stabilisasi fitur ini secara implisit menstabilkan bagian Rust ABI?

Kita berbicara tentang invarian validitas di sini, bukan tata letak data (yang saya anggap Anda rujuk saat memunculkan ABI). Jadi, semua ini tidak akan menstabilkan ABI mana pun. Ini terkait tetapi berbeda, dan pada kenyataannya saat ini sedang ada diskusi tentang ABI serikat pekerja .

Ini terkait tetapi berbeda, dan pada kenyataannya saat ini sedang ada diskusi tentang ABI serikat pekerja.

AFAICT bahasan tersebut adalah tentang representasi memori dari serikat pekerja saja, dan tidak termasuk bagaimana serikat tersebut melewati batas fungsi dan hal-hal lain yang mungkin relevan untuk ABI. Saya tidak berpikir tujuan repo UCG adalah untuk membuat ABI untuk Rust.

Nah, tujuannya adalah untuk mendefinisikan cukup banyak hal untuk interop dengan C. Hal-hal seperti "Rust bool dan C bool kompatibel dengan ABI".

Tapi memang, untuk repr(Rust) , saya pikir tidak ada rencana untuk mendefinisikan pemanggilan fungsi ABI - tapi idealnya itu adalah pernyataan eksplisit dalam bentuk apa pun dokumen yang dihasilkan, bukan hanya kelalaian.

Saya ingin tahu apakah ada beberapa argumen yang menentang pengoptimalan tata letak Option<Foo> mana Foo didefinisikan seperti ini:

union Foo {
   bar: NonZeroUsize,
   baz: &'static str,
}

@Kixunil bisakah Anda mengungkapkannya di https://github.com/rust-rfcs/unsafe-code-guidelines/issues/13? Pertanyaan Anda sebenarnya tidak terkait dengan MaybeUninit .

Saya ingin tahu bagian mana yang akan berisi variabel statis tanpa inisialisasi?
Dalam "C" Saya bisa menulis uint8_t a[100]; di tingkat tinggi file, dan saya tahu bahwa sebuah simbol akan dihukum bagian .bss. Jika saya menulis uint8_t a[100] = {}; simbol yang akan dimasukkan ke bagian DATA (yang akan disalin dari FLASH ke RAM sebelum utama).

Ini adalah contoh kecil di Rust yang menggunakan MaybeUninit:

struct A {
    data: MaybeUninit<[u8; 100]>,
    len: usize,
}

impl A {
    pub const fn new() -> Self {
        Self {
            data: MaybeUninit::uninitialized(),
            len: 0,
        }
    }
}

static mut a: MaybeUninit<[u8; 100]> = MaybeUninit::uninitialized();
static mut b: A = A::new();

Bagian yang akan berisi a dan b simbol?

NB Saya tahu tentang kerusakan simbol tetapi tidak masalah untuk pertanyaan ini.

@ qwerty19106 Dalam contoh Anda, a dan b akan ditempatkan di .bss . LLVM memperlakukan nilai undef , seperti MaybeUninit::uninitialized() , sebagai nol ketika memilih bagian mana variabel akan masuk.

Jika A::new() diinisialisasi len menjadi 1 maka b akan berakhir dengan .data . Jika static mengandung nilai non-nol apapun maka variabel akan masuk .data . Padding diperlakukan sebagai nilai nol.

Inilah yang LLVM lakukan. Rust tidak membuat ~ jaminan ~ promise (*) tentang bagian penaut mana variabel static akan masuk. Ini hanya mewarisi perilaku LLVM.

(*) Kecuali Anda menggunakan #[link_section]

Fakta menyenangkan: Pada titik tertentu, LLVM menganggap undef sebagai nilai bukan nol sehingga variabel a dalam contoh Anda berakhir dengan .data . Lihat # 41315.

Terima kasih @japaric atas jawaban Anda. Hal itu sangat membantuku.

Saya punya ide baru.
Seseorang dapat menggunakan bagian .init_array untuk menginisialisasi variabel mut statis sebelum memanggil main .

Ini adalah bukti konsep:

#[macro_export]
macro_rules! static_singleton {
    ($name_var: ident, $ty:ty, $name_init_fn: ident, $name_init_var: ident, $init_block: block) => {
        static mut $name_var: MaybeUninit<$ty> = unsafe {MaybeUninit::uninitialized()};

        extern "C" fn $name_init_fn() {
            unsafe {
                $init_block
            }
        }

        #[link_section = ".init_array"]
        #[used]
        static $name_init_var: [extern "C" fn(); 1] = [$name_init_fn];
    };
}

Kode tes :

static_singleton!(A, u8, a_init_fn, A_INIT_VAR, {
    let ptr = A.get_mut();
    *ptr = 5;
});

fn main() {
    println!("A inited to {}", unsafe {&A.get_ref()});
}

Hasil : A inited ke 5

Contoh lengkap : taman bermain

Pertanyaan yang belum terselesaikan :
Saya tidak dapat menggunakan concat_idents untuk menghasilkan a_init_fn dan A_INIT_VAR . Sepertinya # 1628 belum siap digunakan.

Tes ini tidak terlalu berguna. Tapi itu bisa berguna di embedded untuk menginisialisasi struct yang rumit (itu akan ditempatkan di .bss , sehingga memungkinkan untuk FLASH ).

Mengapa rustc tidak menggunakan bagian .init_array? Ini adalah bagian standar dari format ELF ( tautan ).

@ qwerty19106 Karena kehidupan sebelum main () dianggap sebagai kesalahan dan secara eksplisit dikeluarkan dari semantik Rust.

Ok, itu desain lang bagus.

Tapi di # [no_std] kami tidak memiliki alternatif yang baik sekarang (mungkin saya mencari yang buruk).

Kita bisa menggunakan spin :: Once , tetapi sangat mahal ( Ordering :: SeqCst pada setiap get referensi).

Saya ingin memiliki pemeriksaan waktu kompilasi pada embedded .

itu sangat mahal ( Ordering::SeqCst pada setiap mendapatkan referensi).

Kedengarannya tidak benar bagi saya. Bukankah semua abstraksi "sekali" seharusnya dilonggarkan saat diakses, dan disinkronkan saat inisialisasi? Atau apakah saya sedang memikirkan hal lain?
cc @Amanieu @alexcrichton

@ muhammaddaffa03 :

Saat Anda mengatakan "tertanam", apakah yang Anda maksud adalah bare-metal? Perlu dicatat bahwa .init_array sebenarnya bukan bagian dari format ELF itu sendiri ¹ - Ini bahkan bukan bagian dari Sistem V ABI ² yang mengembangkannya; hanya .init adalah. Anda tidak akan menemukan .init_array sampai Anda mendapatkan pembaruan draf ABI Sistem V , yang diwarisi dari ABI Linux .

Akibatnya, jika Anda menjalankan dengan bare metal, .init_array bahkan mungkin tidak berfungsi dengan andal untuk kasus penggunaan Anda - bagaimanapun, ini diimplementasikan pada non-bare-metal dengan kode di pemuat dinamis dan / atau libc. Kecuali bootloader Anda bertanggung jawab untuk menjalankan kode yang direferensikan di .init_array , itu tidak akan melakukan apa-apa.

1: Lihat halaman 28, gambar 1-13 "Bagian Khusus"
2: Lihat halaman 63, gambar 4-13 "Bagian Khusus (lanjutan)"

@eddyb Anda membutuhkan paling sedikit Acquire saat membaca Once . Ini adalah beban normal pada x86 dan beban + pagar pada ARM.

Implementasi saat ini menggunakan load(SeqCst) , tetapi dalam praktiknya ini menghasilkan asm yang sama dengan load(Acquire) pada semua arsitektur.

(Apakah Anda keberatan memindahkan diskusi ini ke tempat lain? Mereka tidak ada hubungannya dengan MaybeUninit vs mem :: uninitialized lagi. Keduanya berperilaku sama seperti yang dilakukan LLVM - menghasilkan undef. Apa yang terjadi dengan undef itu nanti tidak menjadi topik di sini. )

Am 13. September 2018 00:59:20 MESZ schrieb Amanieu [email protected] :

@eddyb Anda membutuhkan paling sedikit Acquire saat membaca
Once . Ini adalah beban normal pada x86 dan beban + pagar pada ARM.

Implementasi saat ini menggunakan load(SeqCst) , tetapi dalam praktiknya ini
menghasilkan asm yang sama seperti load(Acquire) pada semua arsitektur.

-
Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung atau lihat di GitHub:
https://github.com/rust-lang/rust/issues/53491#issuecomment -420825802

MaybeUninit telah mendarat di master dan akan berada di malam berikutnya. :)

https://github.com/rust-lang/rust/issues/54470 mengusulkan menggunakan Box<[MaybeUninit<T>]> dalam RawVec<T> . Untuk mengaktifkan ini dan mungkin kombinasi menarik lainnya dengan kotak dan irisan dengan transmutasi yang lebih sedikit, mungkin kita bisa menambahkan beberapa API lagi ke pustaka standar?

Khususnya untuk mengalokasikan tanpa menginisialisasi (saya pikir Box::new(MaybeUninit::uninitialized()) masih akan menyalin size_of::<T>() padding byte?):

impl<T> Box<MaybeUninit<T>> {
    pub fn new_uninit() -> Self {…}
    pub unsafe fn assert_init(s: Self) -> Box<T> { transmute(s) }
}

impl<T> Box<[MaybeUninit<T>]> {
    pub fn new_uninit_slice(len: usize) -> Self {…}
    pub unsafe fn assert_init(s: Self) -> Box<[T]> { transmute(s) }
}

Di core::slice / std::slice , dapat digunakan setelah mengambil sub-slice:

pub unsafe fn assert_init<T>(s: &[MaybeUninit<T>]) -> &[T] { transmute(s) }
pub unsafe fn assert_init_mut<T>(s: &mut [MaybeUninit<T>]) -> &mut [T] { transmute(s) }

Saya pikir Box :: new (MaybeUninit :: uninitialized ()) masih akan menyalin size_of ::() padding byte

Seharusnya tidak, dan ada tes codegen yang dimaksudkan untuk mengujinya.

Byte padding tidak harus disalin karena representasi bitnya tidak masalah (apa pun yang akan mengamati representasi bit adalah UB).

Oke, jadi mungkin Box::new_uninit tidak diperlukan? Namun, versi potongannya berbeda, karena Box::new membutuhkan T: Sized .

Saya ingin menganjurkan agar MaybeUninit::zeroed menjadi const fn . Ada beberapa penggunaan yang berhubungan dengan FFI yang akan saya miliki untuk itu (misalnya statis yang harus diinisialisasi ke nol) dan saya yakin orang lain mungkin menganggapnya berguna. Saya akan dengan senang hati menyumbangkan waktu saya untuk mempertahankan fungsi zeroed .

@mjbshaw Anda harus menggunakan #[rustc_const_unstable(feature = "const_maybe_uninit_zeroed")] untuk itu karena zeroed melakukan hal-hal yang tidak lolos pemeriksaan min_const_fn (https://github.com/rust-lang/ rust / issues / 53555) yang berarti bahwa konstanta MaybeUninit::zeroed tidak akan stabil meskipun fungsinya stabil.

Dapatkah implementasi / stabilisasi ini dipecah menjadi beberapa langkah, agar jenis MaybeUninit tersedia untuk ekosistem yang lebih luas lebih cepat? Langkah-langkahnya bisa:

1) tambahkan MaybeUninit
2) konversi semua penggunaan mem :: uninitialized / zeroed dan deprecate

@tokopedia

tambahkan MaybeUninit

https://doc.rust-lang.org/nightly/core/mem/union.MaybeUninit.html :)

Bagus! Jadi, apakah rencana untuk menstabilkan MaybeUninit secepatnya?

Langkah selanjutnya adalah mencari tahu mengapa https://github.com/rust-lang/rust/pull/54668 mengalami kemunduran yang sangat buruk (dalam beberapa tolok ukur). Saya tidak akan punya banyak waktu untuk melihatnya minggu ini, akan senang jika orang lain bisa melihatnya. : D

Juga saya tidak berpikir kita harus terburu-buru. Kami mendapat API terakhir untuk menangani data yang tidak diinisialisasi dengan salah, jangan terburu-buru dan mengacaukannya lagi. ;)

Meskipun demikian, saya juga memilih untuk tidak menambahkan penundaan yang tidak perlu, sehingga kami akhirnya dapat menghentikan penggunaan footgun lama. :)

Oh, dan hal lain baru saja terlintas di benak saya ... dengan https://github.com/rust-lang/rust/pull/54667 mendarat, API lama sebenarnya melindungi dari beberapa footgun terburuk. Saya ingin tahu apakah kita bisa mendapatkan sebagian dari itu dengan MaybeUninit juga? Ini tidak memblokir stabilisasi, tetapi kami dapat mencoba menemukan cara untuk membuat MaybeUninit::into_inner panik ketika dipanggil pada tipe yang tidak berpenghuni. Dalam pembuatan debug, saya juga bisa membayangkan *x panik ketika x: &[mut] T dengan T tidak berpenghuni.

Pembaruan status: Untuk membuat kemajuan dengan https://github.com/rust-lang/rust/pull/54668, kami mungkin membutuhkan seseorang untuk menyesuaikan komputasi tata letak untuk serikat pekerja. @eddyb bersedia menjadi mentor, tapi kami membutuhkan seseorang untuk melakukan implementasinya. :)

Saya pikir metode yang keluar dari pembungkus, menggantinya dengan nilai yang tidak diinisialisasi, akan berguna:

pub unsafe fn take(&mut self) -> T

Haruskah saya mengirimkan ini?

@shepmaster Ini terasa sangat mirip dengan metode into_inner yang ada. Mungkin kita bisa mencoba menghindari duplikasi di sini?

Juga "mengganti dengan" kemungkinan gambar yang salah di sini, ini seharusnya tidak mengubah konten self sama sekali. Kepemilikan yang adil dipindahkan sehingga sekarang secara efektif dalam keadaan yang sama seperti ketika dibangun tanpa diinisialisasi.

ubah konten self sama sekali

Tentu, jadi implementasinya pada dasarnya akan menjadi ptr::read , tetapi dari sudut pandang penggunaan saya akan mendorong membingkainya sebagai pengganti nilai yang valid dengan nilai yang tidak diinisialisasi.

hindari duplikasi

Saya tidak memiliki keberatan yang kuat, karena saya mengharapkan implementasi yang satu memanggil yang lain. Saya hanya tidak tahu bagaimana keadaan akhirnya.

Saya merasa into_inner adalah nama fungsi yang terlalu polos. Orang-orang, mungkin tanpa membaca dokumen terlalu teliti, masih mendapatkan MaybeUninit::uninitialized().into_inner() . Bisakah kita mengubah nama menjadi sesuatu seperti was_initialized_unchecked atau lebih yang menunjukkan bahwa Anda harus memanggil ini hanya setelah data diinisialisasi?

Saya pikir hal yang sama mungkin berlaku untuk take .

Meskipun agak canggung, sesuatu seperti unchecked_into_initialized mungkin berhasil?

Atau haruskah metode tersebut dihapus seluruhnya dan dokumen memberikan contoh dengan x.as_ptr().read() ?

@SimonSapin into_inner mengkonsumsi self meskipun itu bagus.

Tetapi untuk @shepmaster take , melakukan as_mut_ptr().read() akan melakukan hal yang sama ... meskipun tentu saja lalu mengapa Anda repot-repot dengan pointer yang bisa berubah?

Bagaimana dengan take_unchecked dan into_inner_unchecked ?

Kurasa itu akan menjadi rencana cadangan, tetapi saya lebih suka jika itu bisa menunjukkan bahwa Anda pasti sudah menginisialisasi.

Menempatkan baik penekanan bahwa itu harus diinisialisasi dan deskripsi tentang apa yang dilakukannya (unwrap / into_inner / etc.) Ke dalam satu nama menjadi agak sulit, jadi bagaimana kalau melakukan yang pertama dengan assert_initialized dan meninggalkan yang terakhir tersirat oleh tanda tangan? Kemungkinan unchecked_assert_initialized untuk menghindari menyiratkan pemeriksaan runtime seperti yang dimiliki assert!() .

Kemungkinan unchecked_assert_initialized untuk menghindari pemeriksaan runtime seperti yang dimiliki assert! ().

Kami sudah membedakan antara asumsi dan pernyataan melalui intrinsics::assume(foo) vs assert!(foo) , jadi mungkin assume_initialized ?

assume adalah API yang tidak stabil, contoh stabil dari asumsi vs assert adalah unreachable_unchecked vs unreachable dan get_unchecked vs get . Jadi menurut saya unchecked adalah istilah yang tepat.

Saya akan mengatakan bahwa foo_unchecked hanya masuk akal ketika ada foo , jika tidak, sifat fungsi yang hanya unsafe menunjukkan kepada saya bahwa sesuatu yang "berbeda" sedang terjadi di.

Bikeshed ini jelas warnanya salah

Dengan API khusus ini, kami telah melihat dan akan terus melihat programmer menganggap bahwa ketidakamanan adalah karena "data yang tidak diinisialisasi adalah sampah sehingga Anda dapat menyebabkan UB jika Anda menanganinya dengan sembarangan", daripada yang dimaksudkan "adalah UB yang memanggil ini pada data yang tidak diinisialisasi, titik ". Saya tidak tahu pasti apakah yang bisa dibilang mubazir ⚠️ seperti unchecked akan membantu dengan itu tetapi saya lebih suka berbuat salah di sisi yang lebih membingungkan (= lebih mungkin menyebabkan orang bertanya-tanya atau membaca dokumen dengan sangat hati-hati).

@Ralfian

Saya merasa into_inner adalah nama fungsi yang terlalu polos. Orang-orang, mungkin tanpa membaca dokumen terlalu teliti, masih mendapatkan MaybeUninit::uninitialized().into_inner() . Bisakah kita mengubah nama menjadi sesuatu seperti was_initialized_unchecked atau lebih yang menunjukkan bahwa Anda harus memanggil ini hanya setelah data diinisialisasi?

Saya _ sangat _ menyukai ide ini; Saya merasa kuat bahwa itu mengatakan hal yang benar tentang semantik dan bahwa ini berpotensi berbahaya.

@tokopedia

Menempatkan baik penekanan bahwa itu harus diinisialisasi dan deskripsi tentang apa yang dilakukannya (unwrap / into_inner / etc.) Ke dalam satu nama menjadi agak sulit, jadi bagaimana kalau melakukan yang pertama dengan assert_initialized dan meninggalkan yang terakhir tersirat oleh tanda tangan? Mungkin unchecked_assert_initialized untuk menghindari menyiratkan pemeriksaan runtime seperti yang dimiliki assert!() .

Saya tidak memiliki keraguan tentang nama yang terlalu panjang untuk hal-hal berbahaya. Jika itu membuat lebih banyak orang berpikir dua kali maka bahkan was_initialized_into_inner_unchecked sama sekali tidak masalah bagi saya. Menjadikannya tidak ergonomis (dalam alasan) untuk menulis kode yang tidak aman adalah sebuah fitur, bukan bug;)

Ingatlah bahwa sebagian besar orang kemungkinan besar akan menggunakan IDE dengan beberapa bentuk pelengkapan otomatis, jadi nama yang panjang bukanlah hal yang mudah.

Saya tidak terlalu peduli dengan ergonomi dalam menggunakan fungsi ini, tapi menurut saya di masa lalu nama titik tertentu cenderung disepelekan daripada dibaca, dan nama ini benar-benar harus dibaca untuk memahami apa yang terjadi. Selain itu, saya berharap fungsi ini akan dibahas / dijelaskan hampir sesering yang sebenarnya digunakan (karena relatif khusus dan sangat halus), dan saat mengetik pengenal panjang dalam kode sumber bisa baik-baik saja (misalnya berkat IDE), mengetiknya dari memori dalam sistem obrolan ... kurang bagus (saya setengah bercanda tentang hal ini, tetapi hanya setengah).

@master Yakin; Saya menggunakan IDE dengan penyelesaian otomatis juga; tapi saya pikir nama yang lebih panjang dengan unchecked di dalamnya termasuk di dalam blok unsafe akan memberi saya jeda ekstra.

@tokopedia

mengetiknya dari memori di sistem obrolan ... kurang menyenangkan (saya setengah bercanda tentang hal ini, tapi hanya setengah).

Saya akan membuat trade-off itu. Jika sebuah nama agak istimewa, itu bahkan bisa membuatnya lebih berkesan. ;)

Salah satu dari (atau nama serupa yang memiliki konotasi semantik yang sama):

  • was_initialized_unchecked
  • was_initialized_into_inner_unchecked
  • is_initialized_unchecked
  • is_initialized_into_inner_unchecked
  • was_init_unchecked
  • was_init_into_inner_unchecked
  • is_init_unchecked
  • is_init_into_inner_unchecked
  • assume_initialized_unchecked
  • assume_init_unchecked

baik-baik saja bagiku.

Bagaimana dengan initialized_into_inner ? Atau initialized_into_inner_unchecked , jika menurut Anda unchecked benar-benar diperlukan, meskipun saya cenderung setuju dengan @shepmaster bahwa unchecked hanya diperlukan untuk membedakan dari beberapa varian _checked_ lain yang sama fungsionalitas, di mana pemeriksaan runtime tidak terjadi.

Ketika mengimplementasikan generator pinjaman sendiri secara manual, saya akhirnya menggunakan ptr::drop_in_place(maybe_uninit.as_mut_ptr()) beberapa kali, sepertinya ini akan bekerja dengan baik sebagai metode inheren unsafe fn drop_in_place(&mut self) pada MaybeUninit .

Ada preseden dengan ManuallyDrop::drop .

Saya akan mengatakan bahwa foo_unchecked hanya masuk akal ketika ada foo yang sesuai, jika tidak, sifat fungsi yang tidak aman menunjukkan kepada saya bahwa sesuatu yang "berbeda" sedang terjadi.

Menurut saya tidak memiliki versi aman adalah alasan yang baik untuk menghapus tanda peringatan dari versi yang tidak aman.

hapus tanda peringatan dari versi yang tidak aman

Menjadi anak laki-laki hiperbolik, kapan seharusnya fungsi unsafe tidak memiliki _unchecked terjebak di akhir? Apa gunanya memiliki dua peringatan yang mengatakan hal yang sama?

Itu pertanyaan yang adil. :) Tapi saya pikir jawabannya adalah "hampir tidak pernah", dan saya benar-benar menyesal bahwa kami memiliki offset sebagai fungsi tidak aman pada pointer yang sama sekali tidak menyatakan bahwa itu tidak aman. Tidak harus benar-benar unchecked , tetapi IMO harus ada sesuatu . Ketika saya berada di blok yang tidak aman dan secara tidak sengaja menulis .offset alih-alih .wrapping_offset , saya berjanji kepada kompilator bahwa saya tidak bermaksud untuk membuatnya.

sebagai fungsi tidak aman pada pointer yang sama sekali tidak menyatakan bahwa itu tidak aman

Ini meringkas kekesalan saya pada tahap ini.

@shepmaster jadi menurut Anda tidak realistis bahwa seseorang akan mengedit kode di dalam blok unsafe (mungkin yang besar, mungkin di dalam unsafe fn yang secara implisit memiliki unsafe block), dan tidak menyadari bahwa panggilan yang mereka tambahkan adalah unsafe ?

seseorang akan mengedit kode di dalam unsafe blok [...] dan tidak menyadari bahwa panggilan yang mereka tambahkan adalah unsafe

Maaf, saya tidak bermaksud mengabaikan kemungkinan ini dan itu memang terlihat nyata. Pendapat saya adalah bahwa memasukkan qualifier ke nama suatu fungsi untuk menunjukkan bahwa suatu fungsi tidak aman karena qualifier unsafe ada tidak membantu kami tampaknya menunjukkan kegagalan yang lebih dalam.

Mungkin ini adalah kegagalan yang tidak dapat kami perbaiki dengan cara yang kompatibel ke belakang dan menambahkan kata ke nama adalah satu-satunya solusi yang mungkin, tetapi saya harap bukan itu masalahnya.

mungkin yang besar, mungkin di dalam unsafe fn yang secara implisit memiliki blok unsafe

Saya pernah ditanya mengapa Rust memungkinkan variabel untuk dibayangi karena bayangan jelas merupakan ide yang buruk ketika Anda memiliki fungsi beberapa ratus baris. Saya pribadi sangat meremehkan kasus seperti itu karena saya percaya kode seperti itu secara umum diterima sebagai bentuk yang buruk untuk memulai.

Sekarang, jika beberapa aspek Rust memaksa blok yang tidak aman menjadi lebih besar dari yang "dibutuhkan", mungkin itu juga menunjukkan masalah yang lebih mendasar.


Selain itu, saya ingin tahu apakah IDE + RLS dapat mengidentifikasi fungsi apa pun yang ditandai sebagai tidak aman dan menyorotnya secara khusus. Editor saya telah menyoroti kata kunci unsafe , misalnya.

Sekarang, jika beberapa aspek Rust memaksa blok yang tidak aman menjadi lebih besar dari yang "dibutuhkan", mungkin itu juga menunjukkan masalah yang lebih mendasar.

Nah ada https://github.com/rust-lang/rfcs/pull/2585;)

Selain itu, saya ingin tahu apakah IDE + RLS dapat mengidentifikasi fungsi apa pun yang ditandai sebagai tidak aman dan menyorotnya secara khusus.

Itu bagus! Tidak semua orang hanya membaca kode di IDE - misalnya, review biasanya tidak dilakukan di IDE.

Sekarang, jika beberapa aspek Rust memaksa blok yang tidak aman menjadi lebih besar dari yang "dibutuhkan", mungkin itu juga menunjukkan masalah yang lebih mendasar.

Saya pikir metode tidak aman dalam rantai adalah salah satu contoh yang lebih besar - memisahkan metode di tengah menjadi let -binding bisa sangat tidak ergonomis, tetapi kecuali Anda melakukannya, seluruh rantai akan tercakup.

Tidak cukup 'memaksa', tapi pasti 'memotivasi'.

l ada karat-lang / rfcs # 2585 ;)

Ya, tetapi saya tidak menyebutkannya karena itu juga tidak akan membantu kasus Anda. Orang selalu dapat menambahkan blok unsafe di seluruh tubuh (seperti yang disebutkan di komentar) dan kemudian Anda kembali ke masalah yang sama: panggilan fungsi yang tidak aman "menyelinap masuk".

Tidak semua orang hanya membaca kode di IDE

Ya, itulah sebabnya saya mengesampingkan itu. Saya kira saya seharusnya menyatakan itu dengan lebih jelas.


Saya kira masalah saya adalah, secara efektif , Anda menganjurkan untuk ini:

unsafe fn unsafe_real_name_of_function() { ... }
          ^~~~~~ for humans
^~~~~~           for the compiler

Ini memungkinkan Anda untuk melihat dengan jelas setiap fungsi yang tidak aman saat membaca kode. Pengulangan sangat mengganggu saya dan menunjukkan bahwa ada sesuatu yang kurang optimal.

Ini memungkinkan Anda untuk melihat dengan jelas setiap fungsi yang tidak aman saat membaca kode. Pengulangan sangat mengganggu saya dan menunjukkan bahwa ada sesuatu yang kurang optimal.

Saya mengerti. Anda juga dapat melihat pengulangan ini sebagai penerapan prinsip 4-mata, di mana kompilator menyediakan dua mata. ;)

@shepmaster Saya pikir ini mendapatkan sedikit off-track, tetapi IMO poin aslinya adalah bahwa tidak jelas apa invarian metode ini - yaitu kapan kode unsafe sebenarnya bukan UB , dengan nama yang lebih sederhana.

Saya setuju "tidak dicentang" bukanlah pilihan terbaik, tetapi memiliki preseden sebagai "dengan mudah melanggar invarian".

Ini membuat saya berharap kami memiliki konvensi penamaan di sepanjang baris initialized_or_ub .

Saya pikir ini sedikit keluar jalur

Aku sendiri yang akan mengatakannya. Saya telah mengatakan bagian saya (dan tampaknya tidak ada yang setuju dengan saya), jadi saya akan membiarkannya berbohong; kalian pilih apapun yang kalian mau.

kami memiliki konvensi penamaan di sepanjang baris initialized_or_ub

Maksudmu seperti maybe_uninit(ialized) ? Sesuatu yang entah bagaimana bisa diterapkan secara luas ke sekumpulan metode terkait? 😇

Tidak, maksud saya seperti unwrap_or_else - meletakkan apa yang terjadi di "kasus yang tidak menyenangkan" di nama metode.

@eddyb Hei itu tidak terlalu buruk ... .initialized_or_unsound mungkin?

Secara umum, menambahkan informasi tipe ke nama pengenal dianggap anti-pola (mis. foo_i32 , bar_mutex , baz_iterator ) karena untuk itulah tipe ada.

Namun ketika datang ke fungsi, meskipun unsafe adalah bagian dari tipe fn , menambahkan _unchecked , _unsafe , _you_better_know_what_you_are_doing tampaknya menjadi sangat umum.

Saya bertanya-tanya, mengapa demikian?

Juga, FYI, ada masalah (https://github.com/rust-analyzer/rust-analyzer/issues/190) di rust-analyzer untuk mengungkapkan apakah fungsi unsafe atau tidak. Editor dan IDE harus dapat menekankan operasi yang memerlukan unsafe dalam unsafe blok, yang tidak hanya mencakup pemanggilan fungsi unsafe (terlepas dari apakah mereka diakhiri dengan pengenal seperti, misalnya, _unchecked atau tidak), tetapi juga mendereferensi petunjuk mentah, dll.

Bisa dibilang, rust-analyzer belum dapat melakukan ini (EDIT: intellij-Rust jenis kaleng: https://github.com/intellij-rust/intellij-rust/issues/3013#issuecomment-440442306), tetapi jika maksudnya adalah untuk memperjelas bahwa memanggil ini di dalam blok unsafe memerlukan unsafe , penyorotan sintaks adalah alternatif yang mungkin untuk menyambungkan ini dengan apa pun. Maksud saya, jika Anda benar-benar menginginkan ini sekarang, Anda mungkin dapat menambahkan nama fungsi ini sebagai "kata kunci" ke penyorot sintaks Anda dalam beberapa menit dan menyebutnya sehari.

@bayu_joo

Secara umum, menambahkan informasi tipe ke nama pengenal dianggap anti-pola (mis. foo_i32 , bar_mutex , baz_iterator ) karena untuk itulah tipe ada.

Tentu, notasi Hongaria secara umum dianggap sebagai anti-pola. Saya setuju. Namun, secara umum, keselamatan tidak dipertimbangkan dalam diskusi ini dan saya pikir mengingat bahaya yang ditimbulkan UB ada alasan bagus untuk membuat pengecualian di sini.

Namun ketika datang ke fungsi, meskipun unsafe adalah bagian dari tipe fn , menambahkan _unchecked , _unsafe , _you_better_know_what_you_are_doing tampaknya menjadi sangat umum.

Saya bertanya-tanya, mengapa demikian?

Sederhananya: ketidakamanan. Ketika ketidakpastian terlibat, redundansi adalah urusan teman Anda. Ini berlaku baik untuk kode dan untuk perangkat keras kritis keselamatan dan semacamnya.

Juga, FYI, ada masalah ( rust-analyzer / rust-analyzer # 190 ) di rust-analyzer untuk mengungkapkan apakah fungsinya unsafe atau tidak. Editor dan IDE harus dapat menekankan operasi yang memerlukan unsafe dalam unsafe blok, yang tidak hanya mencakup pemanggilan fungsi unsafe (terlepas dari apakah mereka diakhiri dengan pengenal seperti, misalnya, _unchecked atau tidak), tetapi juga mendereferensi petunjuk mentah, dll.

Bisa dibilang, rust-analyzer belum dapat melakukan ini, tetapi jika tujuannya adalah untuk memperjelas bahwa memanggil ini di dalam blok unsafe membutuhkan unsafe , penyorotan sintaks adalah alternatif yang memungkinkan untuk sufiks ini dengan apapun.

Semua ini sangat mengagumkan. Namun, seperti yang dicatat oleh @RalfJung : " Namun,

Jika pengorbanannya adalah antara menjadi jelek dan cenderung salah (dan karenanya tidak sehat) penggunaan unsafe , saya pikir kita harus selalu memilih yang pertama. Banyak yang bisa dikatakan untuk membuatnya sehingga seorang programmer _has_ berhenti sejenak dan berpikir sendiri, "tunggu, apakah saya melakukan ini dengan benar?"

Misalnya, jika Anda ingin menggunakan operasi kriptografi yang tidak aman di Mundane, Anda harus:

  • Impor dari modul insecure
  • Tulis allow(deprecated) atau hidup dengan peringatan kompilator yang dikeluarkan setiap kali Anda menggunakan operasi itu
  • Tulis kode yang terlihat seperti let mut hash = InsecureSha1::default(); hash.insecure_write(bytes); ...

Semuanya didokumentasikan di sini dengan lebih rinci. Saya tidak berpikir bahwa kita harus _that_ agresif dalam semua keadaan, tetapi menurut saya filosofi yang tepat adalah, jika operasi cukup berbahaya untuk dikhawatirkan, maka tidak boleh ada cara bagi programmer untuk melewatkan gravitasi dari apa yang mereka lakukan.

Saran yang sangat serius

Karena kami 95% khawatir tentang orang-orang yang menyalahgunakan jenis ini, dan hanya 5% khawatir tentang nama yang panjang, mari kita mulai dengan mengganti nama jenis menjadi MaybeUninitialized . 7 karakter tambahan sangat berharga.

Saran yang paling serius

  1. Ubah namanya menjadi MaybeUninitializedOrUndefinedBehavior untuk benar-benar menunjukkannya kepada pengguna akhir.

  2. Pilih tipe ini untuk tidak memiliki metode dan semuanya bisa menjadi fungsi terkait, memperkuat poin pada setiap panggilan fungsi, seperti yang diinginkan:

    MaybeUninitializedOrUndefinedBehavior::into_inner(value)
    

Saran konyol

MaybeUninitializedOrUndefinedBehaviorReadTheDocsAllOfThemYesThisMeansYou

Yah ... sejujurnya, memiliki nama yang panjang seperti MaybeUninitializedOrUndefinedBehavior dalam jenisnya sepertinya salah tempat bagi saya. Ini adalah operasi .into_inner() yang membutuhkan nama baik karena itulah bit yang berpotensi bermasalah yang membutuhkan perhatian ekstra. Tidak memiliki metode bisa menjadi ide yang bagus juga. MaybeUninit::initialized_or_undefined(foo) tampaknya cukup jelas.

IMO kita seharusnya tidak berusaha keras untuk membuat operasi yang tidak aman tidak ergonomis seperti ini. Kami membutuhkan nama ergonomis dan cara untuk menulis kode tidak aman yang benar. Jika kita mengacaukannya dengan sekumpulan nama yang terlalu panjang dan utilitas serta konversi yang tidak jelas, ini akan membuat pengguna enggan menulis kode tidak aman yang benar dan membuat kode yang tidak aman lebih sulit untuk dibaca dan divalidasi.

Ingatlah bahwa sebagian besar orang kemungkinan besar akan menggunakan IDE dengan beberapa bentuk pelengkapan otomatis, jadi nama yang panjang bukanlah hal yang mudah.

Sampai RLS lebih berfungsi, setidaknya bagi saya ini tidak terjadi.

Saya pikir kebanyakan dari kita setuju itu

  • Nama yang lebih deskriptif itu bagus

  • Nama yang kurang ergonomis itu buruk

dan pertanyaannya adalah tentang cara mana untuk menyelesaikan berbagai hal saat ini dalam ketegangan.

Meski begitu saya pikir into_inner secara khusus adalah nama yang buruk untuk metode ini (untuk menggunakan istilah mewah, ini tidak berada di perbatasan Pareto). Ketentuan umum adalah bahwa kita memiliki into_inner ketika Foo<T> berisi tepat satu T , dan Anda ingin mengeluarkannya. Tapi ini tidak benar dari MaybeUninit<T> . Ini berisi nol atau satu T s.

Jadi pilihan yang tidak terlalu buruk, setidaknya, akan menyebutnya unwrap , atau mungkin unwrap_unchecked .

Saya juga berpikir from_initialized atau from_initialized_unchecked terdengar baik-baik saja, meskipun "dari" biasanya muncul di nama metode statis.

Mungkin unwrap_initialized_unchecked akan baik-baik saja?

Sebut saja take_initialized dan jadikan &mut self bukan self . Namanya memperjelas bahwa ia mengharapkan nilai bagian dalam diinisialisasi. Dalam konteks MaybeUninit , unsafe dan fakta bahwa ia tidak mengembalikan Option / Result juga memperjelas bahwa operasi ini tidak dicentang.

Mengambil &mut self tampaknya seperti senjata yang membuatnya lebih mudah untuk kehilangan jejak apakah Anda telah pindah secara semantik dari MaybeUninit .

Nama alternatif: karena ini benar-benar memindahkan kepemilikan seperti metode bernama into menyiratkan, mungkin into_initialized_unchecked ?

Mengambil & mut self tampaknya seperti senjata yang membuatnya lebih mudah untuk kehilangan jejak apakah Anda telah pindah secara semantik dari MaybeUninit.

Ini adalah metode yang telah diminta , dan selama Anda melacak sebaliknya bahwa ini tidak terjadi dua kali, tidak apa-apa.

Dan sepertinya tidak ada gunanya memiliki varian pinjaman dan konsumsi.

Saya suka take_initialized , atau varian yang lebih eksplisit take_initialized_unchecked .

mari kita mulai dengan mengganti nama tipe menjadi MaybeUninitialized

Ada yang siap mempersiapkan PR?

untuk mempersiapkan PR?

Saya dapat menggunakan keterampilan sed saya yang luar biasa karena saya menyarankannya ;-)

Saya pikir itu akan menjadi perbaikan untuk menyebut metode into_inner sesuatu yang menekankan bahwa itu mengasumsikan itu diinisialisasi, tetapi saya pikir penambahan unchecked berlebihan dan tidak membantu. Kami memiliki cara untuk memberi tahu pengguna bahwa fungsi yang tidak aman tidak aman: kami menghasilkan kesalahan kompilator jika mereka tidak menggabungkannya dalam blok yang tidak aman.

EDIT: take_initialized sepertinya bagus

Bagaimana dengan assume_initialized ? Ini:

  • Terhubung ke model 'kewajiban bukti'
  • Berhubungan secara visceral dengan 'asumsi berisiko'
  • Hanya butuh dua kata
  • Menjelaskan arti semantik dari operasi tersebut
  • Membaca dengan sangat alami
  • Sama seperti LLVM assume intrinsik, adalah UB jika salah diasumsikan

Ada yang siap mempersiapkan PR?

Sudahlah. Tim libs memutuskan ini tidak sepadan.

Apakah ada alasan mengapa MaybeUninit<T> bukan Copy sedangkan T: Copy ?

@tommit karena MaybeUninit<T> bergantung pada ManuallyDrop<T> , kami pemrogram harus menjamin bahwa nilai bagian dalam dihapus ketika struct kami berada di luar ruang lingkup. Jika mengimplementasikan Copy , saya pikir akan lebih sulit bagi pendatang baru Rust untuk mengingat untuk menjatuhkan nilai internal T setiap kali, baik dari struct itu sendiri atau salinannya. Dengan cara ini, ini dapat menghasilkan kebocoran memori yang tidak mencolok yang tidak kita duga.

@ luojia65 Tidak yakin bahwa baris penalaran tersebut berlaku jika T itu sendiri adalah Copy , terlepas dari apa yang dilakukan ManuallyDrop dan MaybeUninit .

Saya tidak berpikir ada alasan apapun. Tidak ada yang berpikir untuk menambahkan #[derive(Copy)] ;)

Pengamatan mungkin aspek yang agak halus dari ini:
Saya percaya bahwa meskipun MaybeUninit<T> seharusnya Copy ketika T: Copy , MaybeUninit<T> seharusnya tidak Clone ketika T: Clone dan T bukan Copy .

Oh ya, kita tidak bisa begitu saja memanggil clone .

Saya selalu lupa bahwa Copy: Clone ...

Tidak apa-apa, kita bisa menerapkan Clone for MaybeUninit<T> where T: Copy berdasarkan pengembalian *self .

Saya melakukan yang terbaik untuk memperbarui deskripsi masalah dengan semua pertanyaan yang muncul di sini. Beri tahu saya jika saya melewatkan sesuatu!

Dokumentasi untuk ManuallyDrop::drop mengatakan

Fungsi ini menjalankan destruktor dari nilai yang terkandung dan dengan demikian nilai yang dibungkus sekarang mewakili data yang tidak diinisialisasi. Terserah pengguna metode ini untuk memastikan data yang tidak diinisialisasi tidak benar-benar digunakan.

Adakah saran untuk bagaimana memperbaiki susunan kata tersebut sehingga tidak mungkin tertukar dengan jenis "ketidaktahuan" yang ditangani MaybeUninit ?

Dalam istilah saya, turun ManuallyDrop<T> tidak lagi aman T , tetapi merupakan valid T ... setidaknya sejauh tata letak optimasi peduli.

"basi" / "tidak valid", mungkin? Ini diinisialisasi .

FWIW Saya pikir kata-katanya jelas (setidaknya bagi saya) memastikan bahwa file
objek tidak dijatuhkan dua kali adalah masalah "keamanan". Jika kami memiliki dokumen kecil
dalam UCG mendefinisikan "keamanan" seseorang mungkin harus hyperlink. Anda bisa
tambahkan bahwa T harus berupa definisi "valid" dan hyperlink "valid", tetapi karena kita
belum memiliki definisi ini yang ditulis di mana pun ... Saya tidak tahu. saya
jangan berpikir kita harus memparafrasekannya di seluruh dokumen.

Bisakah kita menstabilkan MaybeUnin sebelum menghentikan yang tidak diinisialisasi?

@RalfJung Menurut saya tempat ini "pindah dari". FWIW kita harus menggunakan terminologi yang sama di std::ptr::read , tapi tidak terlalu jelas juga.

@bluss kita tidak boleh mencela apapun yang banyak digunakan tanpa "lebih baik
solusi ”/“ jalur migrasi ”untuk pengguna saat ini.

Peringatan penghentian seharusnya: "X tidak digunakan lagi, gunakan Y sebagai gantinya". Jika kita
tidak memiliki Y dan X banyak digunakan ... maka kita harus mempertimbangkan untuk bertahan
peringatan penghentian sampai kami memiliki Y.

Jika tidak, kami akan mengirimkan pesan yang sangat aneh.

@cramertj "invalid" bukanlah pilihan yang baik, karena masih (harus!) memenuhi invarian validitas.

Jika kami memiliki dokumen kecil di UCG yang mendefinisikan "keamanan", orang mungkin harus hyperlink. Anda dapat menambahkan bahwa T harus berupa definisi "valid" dan hyperlink "valid", tetapi karena kami belum memiliki definisi ini yang ditulis di mana pun ...

Kami pasti harus melakukannya begitu kami mendapatkan sesuatu: D

@RalfJung Saya rasa "validity invariant" tidak ada dalam kamus sebagian besar (hampir semua?) Pengguna Rust-- Saya pikir mengacu pada "data tidak valid" dalam bahasa sehari-hari dapat diterima ( ManuallyDrop<T> tidak lagi dapat digunakan sebagai a T ). Mengatakan bahwa ia harus menjunjung invarian representasi tertentu yang digunakan compiler untuk pengoptimalan tidak membuatnya menjadi data yang kurang valid.

Saya tidak berpikir "validity invariant" ada dalam kamus kebanyakan (hampir semua?) Pengguna Rust

Cukup adil, istilah tersebut belum resmi (belum). Tapi kita harus memilih istilah resmi untuk ini pada akhirnya, dan kemudian kita harus menghindari bentrokan seperti itu. Kita dapat mengatakan bahwa data yang "valid" adalah yang saya sebut "aman" di posting saya, tetapi kemudian kita memerlukan kata lain untuk apa yang saya sebut "valid".

tulis @shepmaster beberapa waktu lalu

Saya pikir metode yang keluar dari pembungkus, menggantinya dengan nilai yang tidak diinisialisasi, akan berguna:

pub unsafe fn take(&mut self) -> T

Saya rasa perhatian terbesar saya dengan ini adalah dengan fungsi seperti itu, sangat mudah untuk secara tidak sengaja menyalin data non-copy. Jika Anda membutuhkannya, apakah sangat buruk melakukan maybe_uninit.as_ptr().read() ?

Saya pikir saya mungkin telah menyarankan suatu tempat di atas sana untuk memiliki sesuatu seperti take menggantikan sesuatu seperti into_inner . Saya tidak berpikir itu adalah ide yang bagus lagi: Sering kali, pembatasan tambahan bahwa into_inner mengkonsumsi self sebenarnya sangat membantu.

@RalfJung Pada akhirnya, semua metode MaybeUninit tidak aman dan hanya pembungkus kenyamanan sekitar as_ptr . Namun saya berharap take menjadi salah satu operasi yang paling umum, karena MaybeUninit secara efektif hanya Option mana tag dikelola secara eksternal. Ini berguna dalam banyak kasus, misalnya larik di mana tidak semua elemen diinisialisasi (misalnya tabel hash).

Di https://github.com/rust-lang/rust/pull/57045 , saya mengusulkan untuk menambahkan dua operasi baru ke MaybeUninit :

    /// Get a pointer to the first contained values.
    pub fn first_ptr(this: &[MaybeUninit<T>]) -> *const T {
        this as *const [MaybeUninit<T>] as *const T
    }

    /// Get a mutable pointer to the first contained values.
    pub fn first_mut_ptr(this: &mut [MaybeUninit<T>]) -> *mut T {
        this as *mut [MaybeUninit<T>] as *mut T
    }

Lihat PR itu untuk motivasi dan diskusi.

Saat menghapus zeroed sepertinya hanya diganti dengan MaybeUninit::zeroed().into_inner() yang menjadi cara yang setara untuk menulis hal yang sama. Tidak ada perubahan praktis. Dengan nilai uninit, kami memiliki perubahan praktis dari semua data yang tidak diinisialisasi yang disimpan dalam nilai tipe MaybeUninit atau gabungan yang setara.

Untuk alasan ini, saya akan mempertimbangkan untuk menyimpan std::mem::zeroed karena ini adalah fungsi yang banyak digunakan di FFI. Penghentian akan membuatnya mengeluarkan peringatan keras, yang hampir sama dengan itu dihapus, dan setidaknya sangat mengganggu - ini juga dapat menyebabkan bertambahnya jumlah #[allow(deprecated)] yang dapat menyembunyikan masalah lain yang lebih penting.

Latihan mengklarifikasi model dan pedoman Rust untuk kode bertanda unsafe sangat berguna, tetapi mari kita hindari perubahan seperti untuk zeroed mana ia hanya menyusun kembali efek praktis yang sama menggunakan cara baru untuk mengatakannya .

@bluss Pemahaman saya (yang mungkin salah) adalah bahwa std::mem:zeroed sama berbahayanya dengan std::mem::uninitialized , dan kemungkinan besar akan menghasilkan UB. Mungkin ini digunakan untuk menginisialisasi array byte, yang akan lebih baik diinisialisasi dengan vec![0; N] atau [0; N] , dalam hal ini mungkin aturan rustfix dapat ditambahkan untuk mengotomatiskan perubahan? Di luar menginisialisasi array byte atau integer, pemahaman saya adalah bahwa ada kemungkinan bagus bahwa menggunakan std::mem::zeroed dapat mengarah ke UB.

@scottjmaddox Sangat mudah untuk memanggil UB dengan std::mem:zeroed , tetapi tidak seperti std::mem::uninitialized , ada beberapa tipe yang std::mem:zeroed benar-benar valid (mis., tipe asli, banyak yang berhubungan dengan FFI struct s, dll.). Seperti banyak fungsi unsafe , zeroed() tidak boleh digunakan secara sembarangan, tetapi tidak terlalu bermasalah seperti uninitialized() . Saya sendiri akan sedih harus menggunakan MaybeUninit::zeroed().into_inner() daripada std::mem:zeroed() karena tidak ada perbedaan antara keduanya dalam hal keamanan dan versi MaybeUninit lebih berat dan sedikit kurang terbaca (dan ketika saya harus menggunakan kode yang tidak aman, saya sangat menghargai keterbacaan).

@bayu_joo

tidak seperti std :: mem :: uninitialized, ada beberapa tipe yang std :: mem: zeroed benar-benar valid (mis., tipe asli,

Ada beberapa jenis yang mem::uninitialized benar-benar aman ( misalnya unit ), sementara ada beberapa jenis "asli" (misalnya bool , &T , dll. .) yang mem::zeroed memanggil perilaku tidak terdefinisi.


Tampaknya ada kesalahpahaman di sini bahwa MaybeUninit entah bagaimana tentang memori yang tidak diinisialisasi (dan saya dapat melihat mengapa: "Tidak diinisialisasi" ada di namanya).

Bahaya yang kami coba cegah adalah yang disebabkan oleh pembuatan nilai _invalid_, apakah nilai _invalid_ berisi semua nol, atau bit yang tidak diinisialisasi, atau sesuatu yang lain (misalnya bool dari pola bit yang bukan true atau false ), tidak terlalu penting - mem::zeroed dan mem::uninitialized keduanya dapat digunakan untuk membuat nilai _invalid_, dan oleh karena itu sama berbahayanya dari sudut pandang saya.

OTOH MaybeUninit::zeroed() dan MaybeUninit::uninitialized() adalah metode _safe_ karena mereka mengembalikan union . MaybeUninit::into_inner adalah unsafe , dan memanggilnya hanya _safe_ jika prasyarat bahwa bit saat ini di MaybeUninit<T> mewakili nilai _valid_ T terpenuhi. Jika pola bit adalah _invalid_, perilakunya tidak ditentukan. Apakah pola bit tidak valid karena berisi semua nol, bit yang tidak diinisialisasi, atau yang lainnya, tidak terlalu penting.

@RalfJung Saya mulai merasa bahwa nama MaybeUninit mungkin agak menyesatkan. Mungkin kita harus mengganti namanya menjadi MaybeInvalid atau sesuatu seperti itu untuk lebih menyampaikan masalah yang dipecahkannya dan bahaya yang dihindari. EDIT: mengikuti saran @Centril yang saya posting tentang masalah bikeshed.


EDIT: FWIW, menurut saya memiliki cara yang ergonomis (mis. Tanpa langsung menggunakan MaybeUninit ) untuk membuat memori kosong dengan aman akan berguna, tetapi bukan mem::zeroed . Kita dapat menambahkan sifat Zeroed mirip dengan Default yang hanya diimplementasikan untuk tipe yang pola bit semua nolnya valid atau semacamnya, sebagai cara untuk mencapai efek yang mirip dengan apa mem::zeroed sekarang, tapi tanpa kesulitannya.

Secara umum, menurut saya, kita tidak boleh menghentikan fungsionalitas hingga jalur migrasi untuk pengguna saat ini ke solusi yang lebih baik tersedia. MaybeUninit adalah solusi yang lebih baik daripada mem::zeroed di mata saya, meskipun mungkin tidak sempurna (lebih aman, tetapi tidak ergonomis), jadi saya akan baik-baik saja dengan mencela mem::zeroed segera setelah MaybeUninit tanah, bahkan jika pada saat itu terjadi kami tidak memiliki pengganti yang lebih ergonomis.

Mungkin kita harus mengubah namanya menjadi MaybeInvalid atau sesuatu seperti itu untuk menyampaikan masalah yang diselesaikannya dan bahaya yang dihindari dengan lebih baik.

Bikeshed di https://github.com/rust-lang/rust/pull/56138.

@bayu_joo

ada beberapa jenis "asli" (misalnya bool

Selama bool aman untuk FFI (yang secara umum dianggap aman, meskipun RFC 954 ditolak dan kemudian diterima secara tidak resmi), sebaiknya aman menggunakan mem::zeroed untuk itu.

, &T , dll.) Yang mem::zeroed memanggil perilaku tidak terdefinisi.

Ya, tetapi tipe ini yang memiliki UB seharga mem::zeroed juga memiliki UB seharga MaybeUninit::zeroed().into_inner() (Saya berhati-hati untuk dengan sengaja memasukkan .into_inner() dalam komentar asli saya). MaybeUninit tidak menambahkan apa-apa jika pengguna langsung memanggil .into_inner() (persis seperti yang akan saya dan banyak orang lain lakukan jika mem::zeroed tidak digunakan lagi, karena saya hanya menggunakan mem::zeroed untuk tipe yang tidak aman).

Selama bool aman untuk FFI (yang secara umum dianggap aman, meskipun RFC 954 ditolak dan kemudian diterima secara tidak resmi), sebaiknya aman menggunakan mem :: zeroed untuknya.

Saya tidak ingin membahas hal ini secara spesifik, tetapi bool aman bagi FFI dalam arti bahwa ia didefinisikan sama dengan C's _Bool . Namun, nilai true dan false dari C _Bool tidak ditentukan dalam standar C (meskipun mungkin suatu hari nanti, mungkin di C20), jadi apakah mem::zeroed membuat bool valid atau tidak secara teknis ditentukan oleh implementasi.

Ya, tetapi tipe ini yang memiliki UB untuk mem :: zeroed juga memiliki UB untuk MaybeUninit :: zeroed (). Into_inner () (Saya berhati-hati untuk sengaja memasukkan .into_inner () dalam komentar asli saya). MaybeUninit tidak menambahkan apa-apa jika pengguna langsung memanggil .into_inner () (persis seperti yang akan saya dan banyak orang lain lakukan jika mem :: zeroed tidak digunakan lagi, karena saya hanya menggunakan mem :: zeroed untuk jenis yang tidak aman) .

Saya tidak begitu mengerti poin mana yang Anda coba buat di sini. MaybeUninit menambahkan opsi untuk menelepon atau tidak menelepon into_inner , yang tidak dimiliki oleh mem::zeroed , dan ada manfaatnya karena itu adalah operasi yang dapat memperkenalkan perilaku tidak terdefinisi ( membangun serikat sebagai tidak diinisialisasi atau nol aman).

Mengapa ada orang yang secara membabi buta menerjemahkan mem::zeroed menjadi MayeUninit + into_inner ? Itu bukan cara yang tepat untuk "memperbaiki" peringatan penghentian mem::zeroed , dan menonaktifkan peringatan penghentian memiliki efek yang sama dan biaya yang jauh lebih rendah.

Cara yang tepat untuk berpindah dari mem::zeroed ke MaybeUninit adalah dengan mengevaluasi apakah aman untuk memanggil into_inner , dalam hal ini seseorang dapat melakukannya dan menulis komentar yang menjelaskan mengapa itu aman, atau terus bekerja dengan MaybeUninit sebagai union hingga menelepon into_inner menjadi aman (seseorang mungkin perlu mengubah banyak kode sampai itu masalahnya, lakukan pemecah API perubahan untuk mengembalikan MaybeUninit bukan T s, dll.).

Saya tidak ingin membahas hal ini secara spesifik, tetapi bool aman bagi FFI dalam arti bahwa ia didefinisikan sama dengan C's _Bool . Namun, nilai true dan false dari C's _Bool are not defined in the C standard (although they might be some day, maybe in C20), so whether mem :: zeroed creates a valid bool` atau tidak secara teknis sudah ditentukan implementasi .

Permintaan maaf untuk melanjutkan tangen, tetapi C11 mensyaratkan bahwa all-bits-set-to-zero mewakili nilai 0 untuk tipe integer (lihat bagian 6.2.6.2 "Tipe integer", paragraf 5) (yang mencakup _Bool ) . Selain itu, nilai true dan false didefinisikan secara eksplisit (lihat bagian 7.18 "Jenis dan nilai Boolean <stdbool.h> ").

Saya tidak begitu mengerti poin mana yang Anda coba buat di sini. MaybeUninit menambahkan opsi untuk menelepon atau tidak menelepon into_inner , yang tidak dimiliki oleh mem::zeroed , dan ada manfaatnya karena itu adalah operasi yang dapat memperkenalkan perilaku tidak terdefinisi ( membangun serikat sebagai tidak diinisialisasi atau nol aman).

Ada nilai dalam MaybeUninit dan MaybeUninit::zeroed . Kami berdua sepakat tentang itu. Saya tidak memperdebatkan MaybeUninit::zeroed untuk dihapus. Maksud saya adalah bahwa ada juga nilai std::mem::zeroed .

Ada beberapa tipe di mana mem :: uninitialized benar-benar aman (mis. Unit), sementara ada beberapa tipe "native" (mis. Bool, & T, dll.) Yang mem :: zeroed memanggil perilaku tak terdefinisi.

Ini ikan haring merah. Hanya karena zeroed dan uninitialized berlaku untuk beberapa subset tipe tidak membuatnya sebanding dalam penggunaan sebenarnya. Anda perlu melihat ukuran subset tersebut. Jumlah tipe yang valid mem::uninitialized sangat kecil (pada kenyataannya, apakah hanya tipe berukuran nol?), Dan tidak ada yang benar-benar menulis kode yang melakukan itu (misalnya untuk ZST yang hanya Anda gunakan tipe konstruktor). Di sisi lain, ada banyak jenis yang mem::zeroed valid. mem::zeroed berlaku setidaknya untuk jenis berikut (harap saya mengerti):

  • semua tipe integer (termasuk bool , seperti yang disebutkan di atas)
  • semua jenis penunjuk mentah
  • Option<T> mana T memicu pengoptimalan tata letak enum. T termasuk:

    • NonZeroXXX (semua tipe integer)

    • NonNull<U>

    • &U

    • &mut U

    • fn -penunjuk

    • semua array jenis apa pun dalam daftar ini

    • setiap struct mana bidang apapun adalah tipe dalam daftar ini.

  • Array apa saja, struct , atau union hanya terdiri dari tipe-tipe dalam daftar ini.

Ya, uninitialized dan zeroed berhubungan dengan nilai yang berpotensi tidak valid. Namun, programmer menggunakan primitif ini dengan cara yang sangat berbeda.

Pola umum untuk mem::uninitialized adalah:

let val = MaybeUninit::uninitialized();
initialize_value(val.as_mut_ptr()); // or val.set
val.into_inner()

Jika Anda tidak menulis penggunaan nilai yang tidak diinisialisasi dengan cara ini, kemungkinan besar Anda membuat kesalahan besar.

Penggunaan paling umum dari mem::zeroed hari ini adalah untuk jenis yang dijelaskan di atas, dan ini benar-benar valid. Saya sepenuhnya setuju dengan @bluss bahwa saya tidak melihat keuntungan pencegahan senjata api dengan mengganti mem::zeroed() mana-mana dengan MaybeUninit::zeroed().into_inner() .

Untuk meringkas, penggunaan umum uninitialized adalah untuk tipe yang dapat memiliki nilai tidak valid. Penggunaan umum zeroed adalah untuk tipe yang valid jika dikosongkan.

A Zeroed sifat atau serupa (mis. Pod , tetapi perhatikan bahwa T: Zeroed tidak menyiratkan T: Pod ) seperti yang telah disarankan sepertinya hal yang bagus untuk ditambahkan masa depan, tapi jangan menghentikan fn zeroed<T>() -> T sampai kita benar-benar memiliki fn zeroed2<T: Zeroed>() -> T stabil.

@bayu_joo

Permintaan maaf untuk melanjutkan garis singgung, tetapi C11 mensyaratkan itu

Memang! Hanya C ++ bool yang meninggalkan nilai yang valid tidak ditentukan! Terima kasih telah mengoreksi saya, akan mengirim PR ke UCG dengan jaminan ini.

@tokopedia

Anda perlu melihat ukuran subset tersebut. Jumlah tipe yang mem::uninitialized valid sangat kecil (sebenarnya, apakah hanya tipe berukuran nol?), Dan tidak ada yang benar-benar menulis kode yang melakukan itu (mis. Untuk ZST, Anda hanya akan menggunakan tipe konstruktor).

Ini bahkan tidak benar untuk semua ZST jika Anda mempertimbangkan privasi yang memungkinkan untuk memiliki ZST sebagai semacam "bukti kerja" atau "token untuk sumber daya" atau hanya "saksi bukti" secara umum. Contoh sepele :

mod refl {
    use core::marker::PhantomData;
    use core::mem;

    /// Having an object of type `Id<A, B>` is a proof witness that `A` and `B`
    /// are nominally equal type according to Rust's type system.
    pub struct Id<A, B> {
        witness: PhantomData<(
            // Make sure `A` is Id is invariant wrt. `A`.
            fn(A) -> A,
            // Make sure `B` is Id is invariant wrt. `B`.
            fn(B) -> B,
        )>
    }

    impl<A> Id<A, A> {
        /// The type `A` is always equal to itself.
        /// `REFL` provides a proof of this trivial fact.
        pub const REFL: Self = Id { witness: PhantomData };
    }

    impl<A, B> Id<A, B> {
        /// Casts a value of type `A` to `B`.
        ///
        /// This is safe because the `Id` type is always guaranteed to
        /// only be inhabited by `Id<A, B>` types by construction.
        pub fn cast(self, value: A) -> B {
            unsafe {
                // Transmute the value;
                // This is safe since we know by construction that
                // A == B (including lifetime invariance) always holds.
                let cast_value = mem::transmute_copy(&value);

                // Forget the value;
                // otherwise the destructor of A would be run.
                mem::forget(value);

                cast_value
            }
        }
    }
}

fn main() {
    use core::mem::uninitialized;

    // `Id<?A, ?B>` is a ZST; let's make one out of thin air:
    let prf: refl::Id<u8, String> = unsafe { uninitialized() };

    // Segfault:
    let _ = prf.cast(42u8);
}

@Centril ini adalah semacam garis singgung, tetapi saya tidak yakin apakah kode Anda sebenarnya adalah contoh jenis yang memanggil uninitialized membuat nilai yang tidak valid. Anda menggunakan kode yang tidak aman untuk melanggar invarian internal yang seharusnya dipegang oleh Id . Ada banyak cara untuk melakukan ini, misalnya transmute(()) , atau petunjuk mentah jenis casting.

@jethrogb Satu-satunya poin saya adalah bahwa a) harap lebih berhati-hati dengan kata-kata, b) privasi tampaknya tidak cukup masuk akal dalam diskusi tentang apa itu nilai yang valid. Menurut saya, "melanggar invarian internal" dan "nilai tidak valid" adalah hal yang sama; ada kondisi samping di sini "jika A != B maka Id<A, B> tidak berpenghuni.".

Menurut saya, "melanggar invarian internal" dan "nilai tidak valid" adalah hal yang sama; ada kondisi samping di sini "jika A != B maka Id<A, B> tidak berpenghuni.".

Invariants "yang diberlakukan oleh kode perpustakaan" yang berbeda dari invariants "yang dikenakan oleh compiler" dalam beberapa cara, lihat posting blog @RalfJung 's tentang topik . Dalam terminologi itu, contoh Id memiliki invarian keamanan dan mem::zeroed atau cara lain untuk secara umum mensintesis Id<A, B> tidak dapat aman , tetapi tidak langsung UB hanya membuat nilai Id dengan mem::zeroed atau mem::uninitialized karena Id tidak memiliki invarian validitas . Meskipun pembuat kode yang tidak aman tentunya perlu mengingat kedua jenis invarian tersebut, ada beberapa alasan mengapa diskusi ini sebagian besar berfokus pada validitas:

  • Invarian keamanan ditentukan oleh pengguna, jarang diformalkan, dan dapat menjadi rumit sewenang-wenang, sehingga ada sedikit harapan untuk bernalar secara umum tentang mereka atau kompilator / bahasa membantu menegakkan invarian keamanan tertentu.
  • Memecah invarian keamanan terkadang diperlukan (secara internal di dalam perpustakaan suara), jadi meskipun kita secara mekanis dapat mengesampingkan mem::zeroed::<T>() berdasarkan invarian keamanan T , kita mungkin tidak mau.
  • Terkait, konsekuensi dari invarian validitas yang rusak dalam beberapa hal lebih buruk daripada invarian keamanan yang rusak (lebih sedikit kesempatan untuk men-debugnya karena semua akan segera lepas, dan seringkali perilaku aktual yang dihasilkan dari UB kurang dapat dipahami karena semua penyusun dan pengoptimal faktor ke dalamnya, sedangkan invarian keamanan hanya dieksploitasi secara langsung oleh kode dalam modul / peti yang sama).

Setelah membaca komentar @jethrogb , saya setuju bahwa mem::zeroed tidak boleh ditinggalkan dengan pengenalan MaybeUninit .

@jethrogb Kecil nit:

semua array jenis apa pun dalam daftar ini
struct apa pun di mana bidang apa pun adalah tipe dalam daftar ini.

Tidak yakin apakah ini kesalahan ketik sederhana atau perbedaan semantik, tetapi saya pikir Anda perlu mengatasi kedua peluru ini-- Saya tidak yakin bahwa None misalnya Option<[&u8; 2]> memiliki bitwise-zero sebagai representasi yang valid (misalnya dapat menggunakan [0, 24601] sebagai representasi dari None case-- hanya satu dari nilai dalam yang harus menggunakan representasi niche - cc @ eddyb untuk memeriksa saya tentang ini). Saya ragu kita melakukan ini hari ini, tetapi tampaknya tidak sepenuhnya mustahil bahwa hal seperti ini dapat muncul di masa depan.

@tokopedia

Penggunaan paling umum dari mem :: zeroed hari ini adalah untuk tipe yang dijelaskan di atas, dan ini sangat valid.

Apakah ada sumber untuk ini?

Di sisi lain, ada banyak tipe yang mem :: zeroed valid.

Ada juga banyak sekali kasus yang dapat digunakan secara tidak benar.

Saya memahami bahwa bagi mereka yang menggunakan mem::zeroed berat dan benar, menunda penghentian hingga solusi yang lebih ergonomis tersedia adalah alternatif yang sangat menarik.

Saya lebih suka trade-off untuk mengurangi atau menghilangkan jumlah penggunaan yang salah dari mem::zeroed bahkan jika itu menimbulkan biaya ergonomis sementara. Penghentian memperingatkan pengguna bahwa apa yang mereka lakukan berpotensi memicu perilaku yang tidak ditentukan (terutama pengguna baru yang menggunakannya untuk pertama kali), dan kami memiliki solusi yang tepat untuk apa yang harus dilakukan, yang membuat peringatan dapat ditindaklanjuti.

Saya sering menggunakan MaybeUninit dan kurang ergonomis untuk digunakan daripada mem::zeroed dan mem::uninitialized , tetapi hal itu tidak terlalu mengganggu saya. Jika MaybeUninit sesakit yang diklaim beberapa komentar dalam diskusi ini, maka perpustakaan dan / atau RFC untuk alternatif mem::zeroed akan segera muncul (tidak ada yang memblokir siapa pun di sini AFAICT).

Atau, pengguna dapat mengabaikan peringatan dan tetap menggunakan mem::zeroed , itu terserah mereka, kami tidak akan pernah bisa menghapus mem::zeroed dari libcore .

Tetapi orang yang menggunakan mem::zeroed berat harus secara aktif memeriksa apakah semua penggunaan mereka benar. Terutama mereka yang sangat banyak menggunakan mem::zeroed , mereka yang menggunakannya dalam kode umum, mereka yang menggunakannya sebagai alternatif yang "tidak terlalu menakutkan" untuk mem::uninitialized , dll. Menunda penghentian hanya akan menunda peringatan pengguna bahwa apa yang mereka lakukan mungkin perilaku yang tidak terdefinisi.

@buss

Saat menghapus zeroed sepertinya hanya diganti oleh MaybeUninit :: zeroed (). Into_inner () yang menjadi cara yang setara untuk menulis hal yang sama. Tidak ada perubahan praktis. Dengan nilai uninit, kami memiliki perubahan praktis dari semua data yang tidak diinisialisasi disimpan dalam nilai jenis MaybeUninit atau union yang setara.

Ini benar ketika kita berbicara tentang integer, tetapi begitu kita melihat misalnya jenis referensi, mem::zeroed() menjadi masalah.

Namun, saya setuju bahwa kemungkinan besar orang akan benar-benar menyadari bahwa mem::zeroed::<&T>() adalah masalah, daripada orang yang menyadari bahwa mem::uninitialized::<bool>() adalah masalah. Jadi mungkin masuk akal untuk menyimpan mem::zeroed() .

Perhatikan, bagaimanapun, bahwa kita mungkin masih memutuskan bahwa mem::uninitialized::<u32>() baik-baik saja - jika kita mengizinkan bit yang tidak diinisialisasi dalam tipe integer, mem::uninitialized() menjadi valid untuk hampir semua "tipe POD". Saya tidak berpikir kita harus mengizinkan ini, tetapi kita masih harus melakukan diskusi ini.

Jumlah tipe yang valid mem :: uninitialized sangat kecil (pada kenyataannya, apakah hanya tipe berukuran nol?), Dan tidak ada yang benar-benar akan menulis kode yang melakukan itu (misalnya untuk ZST Anda hanya akan menggunakan tipe konstruktor).

FWIW, beberapa kode iterator irisan sebenarnya harus membuat ZST dalam kode generik tanpa bisa menulis konstruktor tipe. Ini menggunakan mem::zeroed() / MaybeUninit::zeroed().into_inner() untuk itu.

mem::zeroed() berguna untuk kasus FFI tertentu di mana Anda diharapkan nol nilai dengan memset(&x, 0, sizeof(x)) sebelum memanggil fungsi C. Saya pikir ini adalah alasan yang cukup untuk membuatnya tidak digunakan lagi.

@Aieuieu Yang tampaknya tidak perlu. Konstruksi Rust yang cocok dengan memset adalah write_bytes .

mem :: zeroed () berguna untuk kasus FFI tertentu

Juga, terakhir kali saya memeriksa, mem::zeroed adalah cara idiomatis untuk menginisialisasi libc struktur dengan bidang pribadi atau platform-dependent.

@RalfJung Kode lengkap yang dimaksud biasanya Type x; memset(&x, 0, sizeof(x)); dan bagian pertama tidak memiliki padanan Rust yang hebat. Menggunakan MaybeUninit untuk pola ini adalah banyak gangguan baris (dan codegen yang jauh lebih buruk tanpa pengoptimalan) ketika memori tidak pernah benar-benar tidak valid setelah memset .

Saya punya pertanyaan tentang desain MaybeUninit : Adakah cara untuk menulis ke satu bidang dari T terkandung di dalam MaybeUninit<T> sedemikian rupa sehingga Anda dapat menulis ke semua kolom dan berakhir dengan tipe yang valid / diinisialisasi?

Misalkan kita memiliki struct seperti berikut:

// Let us suppose that Foo can in principle be any struct containing arbitrary types
struct Foo {bar: bool, baz: String}

Apakah membuat referensi & mut Foo, dan kemudian menulisnya memicu UB?

main () {
    let uninit_foo = MaybeUninitilized::<Foo>::uninitialized();
    unsafe { *uninit_foo.get_mut().bar = true; }
    unsafe { *uninit_foo.get_mut().baz = "hello world".to_owned(); }
}

Apakah menggunakan pointer mentah daripada referensi menghindari masalah ini?

main () {
    let uninit_foo = MaybeUninitilized::<Foo>::uninitialized();
    unsafe { *uninit_foo.as_mut_pointer().bar = true; }
    unsafe { *uninit_foo.as_mut_pointer().baz = "hello world".to_owned(); }
}

Ataukah ada cara lain di mana pola ini dapat diterapkan tanpa memicu UB? Secara intuitif, bagi saya tampaknya selama saya tidak membaca memori yang tidak diinisialisasi / tidak valid, maka semuanya akan baik-baik saja, tetapi beberapa komentar di utas ini membuat saya meragukannya.

Kasus penggunaan saya untuk fungsi ini adalah untuk pola pembangun di tempat untuk jenis di mana beberapa bidang harus ditentukan oleh pengguna (dan tidak memiliki default yang masuk akal), tetapi beberapa bidang memang memiliki default nilai.

Apakah ada cara untuk menulis ke satu bidang T yang ada di dalam MaybeUninitsehingga Anda bisa dari waktu ke waktu menulis ke semua kolom dan berakhir dengan tipe yang valid / diinisialisasi?

Iya. Menggunakan

ptr::write(&mut *(uninit.as_mut_ptr()).bar, val1);
ptr::write(&mut *(uninit.as_mut_ptr()).baz, val2);
...

Anda tidak boleh menggunakan get_mut() untuk ini, itulah mengapa dokumen untuk get_mut mengatakan bahwa nilai harus diinisialisasi sebelum memanggil metode ini. Kami mungkin melonggarkan aturan itu di masa mendatang, yang sedang dibahas di https://github.com/rust-rfcs/unsafe-code-guidelines/.

@RalfJung Bukankah *(uninit.as_mut_ptr()).bar = val1; berisiko menjatuhkan nilai sebelumnya di bar , yang mungkin tidak diinisialisasi? Saya pikir itu perlu dilakukan

ptr::write(&mut (*uninit.as_mut_ptr()).bar, val1);

@scottjmaddox ah, benar. Saya lupa tentang Drop . Saya akan memperbarui posting.

Dengan cara apa varian penulisan ke bidang yang tidak diinisialisasi ini menunjukkan perilaku yang kurang terdefinisi daripada get_mut() ? Pada titik kode di mana argumen pertama untuk ptr::write dievaluasi, kode telah membuat &mut _ ke bidang dalam yang seharusnya sama tidak terdefinisi seperti referensi ke seluruh struct yang seharusnya dibuat. Haruskah kompilator tidak diizinkan untuk menganggap ini sudah dalam keadaan diinisialisasi?

Bukankah itu memerlukan metode proyeksi-penunjuk baru yang tidak memerlukan perantara &mut _ diekspos?


Contoh yang sedikit menarik:

pub struct A { inner: bool }

pub fn init(mut uninit: MaybeUninit<A>) -> A {
    unsafe {
        let mut previous: [u8; std::mem::size_of::<bool>()] = [0];

        {
            // Doesn't the temorary reference assert inner was in valid state before?
            let inner_ptr: *mut _ = &mut (*uninit.as_mut_ptr()).inner;
            ptr::copy(inner_ptr as *const [u8; 1], (&mut previous) as *mut _, 1);

            // With the assert below, couldn't the compiler drop this?
            std::ptr::write(inner_ptr, true);
        }

        // Assert Inner wasn't false before, so it must have been true already!
        assert!(previous[0] != 0);

        // initialized all fields, good to proceed.
        uninit.into_inner()
    }
}

Tetapi jika kompilator menganggap &mut _ sebagai representasi yang valid, ia mungkin langsung membuang ke ptr::write ? Jika kita melewati assert, isinya bukan 0 tetapi satu-satunya bool valid lainnya adalah true/1 . Jadi bisa diasumsikan ini sebagai no-op jika kita melewati assert. Karena nilainya tidak diakses sebelumnya, setelah menata ulang kita bisa berakhir dengan ini? Sepertinya llvm tidak mengeksploitasi ini sekarang, tapi saya sangat tidak yakin apakah ini akan dijamin.


Jika kita malah membuat MaybeUninit kita sendiri di dalam fungsi, kita mendapatkan kenyataan yang sedikit berbeda. Di taman bermain kami malah mengetahuinya mengasumsikan bahwa pernyataan tidak pernah dapat memicu, mungkin karena mengasumsikan str::ptr::write adalah satu-satunya penulisan ke inner sehingga itu pasti sudah terjadi sebelum kita membaca dari previous ? Ini sepertinya agak mencurigakan. Untuk mendukung teori ini, perhatikan apa yang terjadi saat Anda mengubah pointer ke false .


Saya menyadari masalah pelacakan ini mungkin bukan tempat terbaik untuk pertanyaan ini.

@RalfJung @scottjmaddox Terima kasih atas jawaban Anda. Nuansa ini persis mengapa saya bertanya.
@HeroicKatora Ya, saya bertanya-tanya tentang itu.

Mungkin mantra yang benar adalah ini?

struct Foo {bar: bool, baz: String}

fn main () {
    let mut uninit_foo = MaybeUninit::<Foo>::uninitialized();
    unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).bar) as *mut bool, true); }
    unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).baz) as *mut String, "".to_string()); }
}

( taman bermain )

Saya membaca komentar di Reddit (yang sayangnya tidak dapat saya temukan lagi) yang menyarankan agar segera memberikan referensi ke pointer ( &mut foo as *mut T ) sebenarnya dikompilasi hanya untuk membuat pointer. Namun, bit *uninit_foo.as_mut_ptr() membuat saya khawatir. Apakah boleh untuk mendereferensi penunjuk ke memori unitial seperti ini? Kami tidak benar-benar membaca apa pun, tetapi tidak jelas bagi saya apakah kompiler tahu itu.

Saya pikir varian unaligned dari ptr::write mungkin diperlukan untuk kode umum di atas MaybeUninit<T> karena tidak semua jenis akan memiliki bidang yang sejajar?

Tidak perlu write_unaligned . Kompilator menangani perataan bidang untuk Anda. Dan as *mut bool seharusnya juga tidak diperlukan, karena kompilator dapat menyimpulkan bahwa ia perlu memaksa &mut menjadi *mut . Saya pikir paksaan yang disimpulkan ini adalah mengapa aman / valid. Jika Anda ingin eksplisit dan melakukan as *mut _ , itu juga tidak masalah. Jika Anda ingin menyimpan pointer dalam sebuah variabel, maka perlu dilakukan paksaan menjadi sebuah pointer.

@scottjmaddox Apakah ptr::write masih aman meskipun strukturnya #[repr(packed)] ? ptr::write mengatakan pointer harus disejajarkan dengan benar, jadi saya berasumsi ptr::write_unaligned diperlukan dalam kasus di mana Anda menulis beberapa kode umum yang perlu menangani representasi yang dikemas (meskipun sejujurnya saya tidak yakin Saya dapat memikirkan contoh "kode generik di atas MaybeUninit<T> " yang tidak akan tahu apakah bidang tersebut diratakan dengan benar atau tidak).

@tokopedia

yang menyarankan bahwa segera mentransmisikan referensi ke pointer (& mut foo as * mut T) sebenarnya mengkompilasi hanya untuk membuat pointer.

Apa yang dikompilasinya berbeda dari semantik yang diizinkan digunakan oleh kompiler untuk melakukan kompilasi ini. Meskipun ini adalah tanpa operasi di IR, ini masih bisa memiliki efek semantik seperti menyatakan asumsi tambahan ke kompilator. @scottjmaddox benar di mana operasi sedang dimainkan di sini tetapi bagian penting dari pertanyaannya adalah pembuatan referensi yang bisa berubah yang terjadi sebelum dan secara independen dari paksaan ref-to-ptr. Kemudian @mjbshaw secara teknis benar tentang keamanan umum yang membutuhkan ptr::write_unaligned ketika argumennya adalah argumen umum yang tidak diketahui.

Saya tidak ingat di mana saya membaca ini (nomikon? Salah satu kiriman blog

Dengan cara apa varian penulisan ke bidang yang tidak diinisialisasi ini menunjukkan perilaku yang kurang terdefinisi daripada get_mut ()? Pada titik kode di mana argumen pertama ke ptr :: write dievaluasi, kode telah membuat & mut _ ke bidang dalam yang seharusnya sama tidak terdefinisi sebagai referensi ke seluruh struct yang seharusnya dibuat. Haruskah kompilator tidak diizinkan untuk menganggap ini sudah dalam keadaan diinisialisasi?

Pertanyaan yang sangat bagus! Kekhawatiran ini adalah salah satu alasan mengapa saya membuka https://github.com/rust-lang/rfcs/pull/2582. Dengan diterima RFC, kode yang saya tunjukkan tidak membuat &mut , itu menciptakan *mut .

@mjb Touché. Ya, saya kira Anda benar tentang kemungkinan struct sedang dikemas, dan oleh karena itu membutuhkan ptr::write_unaligned . Saya belum pernah memikirkan itu sebelumnya, terutama karena saya belum pernah menggunakan struktur yang padat dari karat. Ini mungkin akan menjadi serat yang clippy, jika belum.

Edit: Saya tidak melihat clippy lint yang relevan, jadi saya mengirimkan masalah: https://github.com/rust-lang/rust-clippy/issues/3659

Saya membuka PR untuk menghentikan penggunaan mem::zeroed : https://github.com/rust-lang/rust/pull/57825

Saya telah membuka masalah di repo RFC untuk membagi diskusi tentang pengosongan memori yang aman, sehingga kami dapat menghentikan mem::zeroed di beberapa titik setelah kami memiliki solusi yang lebih baik untuk masalah itu: https://github.com / rust-lang / rfcs / issues / 2626

Apakah mungkin untuk menstabilkan const uninitialized , as_ptr dan
as_mut_ptr sebelum API lainnya? Tampaknya sangat mungkin bagi saya bahwa ini
akan stabil seperti sekarang. Selain itu, API lainnya dapat digunakan
atas as_ptr dan as_mut_ptr , jadi setelah distabilkan akan memungkinkan untuk
memiliki sifat MaybeUninitExt di crates.io yang menyediakan, di stable, API
yang saat ini sedang dibahas membiarkan lebih banyak orang (misalnya pengguna yang hanya stabil)
berikan umpan balik tentang itu.

Di dalam tertanam, alih-alih pengalokasi global (tidak stabil), kami menggunakan variabel statis,
banyak . Tanpa MaybeUninit tidak ada cara untuk memiliki memori yang tidak diinisialisasi
variabel statis di stabil. Ini mencegah kami menempatkan kapasitas tetap
koleksi dalam variabel statis dan menginisialisasi variabel statis saat runtime, di
tanpa biaya. Menstabilkan subset API ini akan membebaskan kasus penggunaan ini.

Untuk memberi Anda gambaran tentang betapa pentingnya hal ini bagi komunitas tertanam, kami melakukannya
[survei] menanyakan komunitas tentang poin rasa sakit dan kebutuhan mereka. Menstabilkan
MaybeUninit keluar sebagai hal kedua yang paling banyak diminta untuk distabilkan (di belakang
const fn dengan batas sifat) dan, secara keseluruhan, berakhir di tempat ke-7 dari lusinan
permintaan terkait rust-lang / *. Setelah pertimbangan lebih lanjut dalam WG kami bertemu
prioritasnya, secara keseluruhan, tempat ketiga karena dampaknya yang diharapkan terhadap ekosistem.

(Pada catatan yang lebih pribadi, saya adalah penulis kerangka konkurensi tertanam
yang akan mendapatkan keuntungan dari penggunaan internal MaybeUninit (penggunaan memori dalam format
aplikasi dapat dikurangi 10-50% dengan nol perubahan pada kode pengguna). saya
dapat menyediakan fitur Kargo khusus malam untuk ini, tetapi setelah bertahun-tahun
hanya setiap malam tertanam dan baru-baru ini membuatnya stabil saya merasakan itu
menyediakan fitur khusus malam akan menjadi pesan yang salah untuk dikirim ke pengguna saya
jadi saya sangat menantikan API ini distabilkan.)

@japaric Itu pasti akan menghindari diskusi penamaan sekitar into_inner dan teman-teman. Namun, saya masih prihatin tentang diskusi semantik, misalnya tentang orang yang melakukan let r = &mut *foo.as_mut_ptr(); dan karenanya menegaskan bahwa mereka memiliki referensi yang valid, sementara kami belum yakin apa persyaratan validitas untuk referensi itu - yaitu, kami belum bisa dipastikan apakah memiliki referensi terhadap data yang tidak valid adalah insta-UB. Untuk contoh konkret:

let x: MaybeUninit<!> = MaybeUninit::uninitialized();
let r: &! = &*x.as_ptr() // is this UB?

Diskusi ini baru saja dimulai di UCG WG.

Harapan saya adalah hal itu dapat menstabilkan MaybeUninit dalam satu "paket" yang koheren dengan cerita yang tepat untuk data yang tidak diinisialisasi, sehingga orang hanya perlu mempelajari kembali hal-hal ini sekali, daripada merilisnya sedikit demi sedikit sepotong dan mungkin harus mengubah beberapa aturan di sepanjang jalan. Tapi mungkin itu bukan ide yang bagus, dan lebih penting kita mengeluarkan sesuatu untuk meningkatkan status quo?

Tapi bagaimanapun saya pikir kita seharusnya tidak menstabilkan apa pun sebelum kita menerima https://github.com/rust-lang/rfcs/pull/2582 , jadi setidaknya kita bisa memberi tahu orang-orang dengan pasti bahwa berikut ini bukan UB:

let x: MaybeUninit<(!, u32)> = MaybeUninit::uninitialized();
let r1: *const ! = &(*x.as_ptr()).1; // immediately coerced to raw ptr, no UB
let r2 = &(*x.as_ptr()).1 as *const !; // immediately cast to raw ptr, no UB

(Perhatikan bahwa seperti biasa ! adalah ikan haring merah di sini, dan semua contoh dalam posting ini kami sama, UB-bijaksana, jika kita menggunakan bool sebagai gantinya.)

Harapan saya adalah hal itu dapat menstabilkan MaybeUninit dalam satu "paket" yang koheren dengan cerita yang tepat untuk data yang tidak diinisialisasi, sehingga orang hanya perlu mempelajari kembali hal-hal ini sekali, daripada merilisnya sepotong demi sepotong dan mungkin harus ubah beberapa aturan di sepanjang proses.

Menurut saya argumen ini sangat menarik.

Saya pikir kebutuhan yang paling mendesak adalah memiliki beberapa pesan yang jelas tentang bagaimana menangani memori yang tidak diinisialisasi tanpa UB. Jika saat ini hanya "gunakan pointer mentah dan ptr::read_unaligned dan ptr::write_unaligned ", maka tidak apa-apa, tapi kami pasti membutuhkan beberapa cara yang jelas untuk mendapatkan pointer mentah ke nilai tumpukan yang tidak diinisialisasi dan ke bidang struct / tuple . rust-lang / rfcs # 2582 (ditambah beberapa dokumentasi) tampaknya memenuhi kebutuhan yang mendesak, sedangkan MaybeUninit tidak.

@scottjmaddox bagaimana RFC itu tetapi tanpa MaybeUninit ada gunanya untuk memori yang tidak diinisialisasi (stack)?

@RalfJung Saya kira itu tergantung apakah yang berikut ini adalah UB atau tidak:

let x: bool = mem::uninitialized();
ptr::write(&x as *mut bool, false);
assert_eq!(x, false);

Asumsi implisit saya adalah bahwa rust-lang / rfcs # 2582 akan membuat contoh di atas valid dan terdefinisi dengan baik. Apakah bukan ini masalahnya?

@tokopedia

let x: bool = mem::uninitialized();

Ini UB. Ini tidak ada hubungannya dengan referensi.

Asumsi implisit saya adalah bahwa rust-lang / rfcs # 2582 akan membuat contoh di atas valid dan terdefinisi dengan baik.

Saya sangat terkejut dengan ini. RFC itu hanya tentang referensi. Mengapa Anda menganggap itu mengubah sesuatu tentang boolean?

@Ralfian

Ini UB. Ini tidak ada hubungannya dengan referensi.

Dokumentasi untuk mem :: uninitialized () mengatakan:

Bypasses pemeriksaan inisialisasi memori normal Rust dengan berpura - pura

Dokumentasi tidak mengatakan apa-apa tentang T* .

@kpp Apa yang ingin kamu katakan? Tidak ada * dan tidak ada & dalam satu baris kode itu:

let x: bool = mem::uninitialized();

Mengapa Anda mengklaim baris ini sebagai UB?

Karena bool harus selalu true atau false , dan yang ini tidak. Lihat juga https://github.com/rust-rfcs/unsafe-code-guidelines/blob/master/reference/src/glossary.md#validity -and-safety-invariant.

@kpp untuk pernyataan itu agar mendefinisikan perilaku mem::uninitialized perlu mewujudkan _valid_ bool .

Pada semua platform yang saat ini didukung bool hanya memiliki dua nilai _valid_, true (pola bit: 0x1 ) dan false (pola bit: 0x0 ).

mem::uninitialized , bagaimanapun, menghasilkan pola bit di mana semua bit memiliki nilai uninitialized . Pola bit ini bukan 0x0 atau 0x1 , oleh karena itu, hasil bool adalah _invalid_, dan perilakunya tidak terdefinisi.

Untuk membuat perilaku terdefinisi kita perlu mengubah definisi bool untuk mendukung tiga nilai yang valid: true , false , atau uninitialized . Namun, kami tidak dapat melakukan itu, karena T-lang dan T-compiler sudah RFC menyatakan bahwa bool identik dengan C _Bool dan kami tidak dapat merusak jaminan itu (ini memungkinkan bool untuk digunakan secara portabel di C FFI).

Bisa dibilang, C tidak memiliki definisi validitas yang sama persis dengan yang dimiliki Rust, tetapi "representasi perangkap" C sangat mendekati. Singkatnya, tidak banyak yang dapat dilakukan di C dengan _Bool yang nilainya tidak mewakili true atau false tanpa menggunakan perilaku tidak terdefinisi.

Jika Anda benar maka kode aman berikut harus UB juga:

let x: bool;
x = true;

Yang jelas tidak.

Jika Anda benar maka kode aman berikut harus UB juga:

let x: bool; tidak menginisialisasi x menjadi pola bit uninitialized , tidak menginisialisasi x sama sekali. x = true; menginisialisasi x (catatan: jika Anda tidak menginisialisasi x sebelum menggunakannya, Anda mendapatkan kesalahan kompilasi).

Ini berbeda dari perilaku C, di mana, bergantung pada konteks, _Bool x; menginisialisasi x ke nilai _indeterminate_.

Tidak, di sana kompilator tahu bahwa x belum diinisialisasi.

Masalah dengan mem::uninitialized adalah bahwa hal itu menginisialisasi variabel, sejauh compiler pelacakan inisialisasi yang bersangkutan.

let x: bool; tidak dengan sendirinya bahkan mencadangkan ruang apa pun untuk x untuk disimpan, itu hanya menyimpan nama. let x = foo; mencadangkan sebagian ruang dan menginisialisasinya menggunakan foo . let x: bool = mem::uninitialized(); mencadangkan 1 byte ruang untuk x tetapi membiarkannya tidak diinisialisasi, dan itu merupakan masalah.

Ini adalah cara yang mudah untuk merekam API yang dirancang untuk kaki Anda sehingga harus didokumentasikan baik di mem :: uninitialized dan intrinsics :: uninit dengan spesialisasi untuk mem :: uninitializedpanik selama kompilasi.

Apakah ini juga berarti bahwa menginisialisasi struct apa pun dengan bool di dalamnya dengan mem :: uninitialized adalah UB juga?

@ppp

Apakah ini juga berarti bahwa menginisialisasi struct apa pun dengan bool di dalamnya dengan mem :: uninitialized adalah UB juga?

Ya - seperti yang mungkin Anda temukan, mem::uninitialized membuatnya mudah untuk menembak diri sendiri di kaki, saya akan mengatakan bahwa hampir tidak mungkin untuk menggunakan dengan benar. Itulah mengapa kami mencoba untuk menghentikannya demi MaybeUninit , yang sedikit lebih bertele-tele untuk digunakan, tetapi memiliki keuntungan bahwa, karena ini adalah gabungan, Anda dapat menginisialisasi nilai "dengan bagian" tanpa benar-benar terwujud nilai itu sendiri dalam status _invalid_. Nilainya hanya harus _valid_ sepenuhnya pada saat seseorang memanggil into_inner() .

Anda mungkin tertarik membaca bagian dari nomikon tentang inisialisasi yang dicentang dan tidak dicentang (tidak): https://doc.rust-lang.org/nomicon/checked-uninit.html Mereka membahas cara inisialisasi let x: bool; bekerja di Rust yang aman. Silakan isi masalah jika penjelasannya tidak jelas atau ada hal yang tidak Anda mengerti. Juga perlu diingat bahwa sebagian besar penjelasan di sana bersifat "non-normatif" karena belum melalui proses RFC. Pedoman Kode Tidak Aman WG akan mencoba untuk mengirimkan RFC yang mendokumentasikan dan menjamin perilaku saat ini sekitar tahun ini.

Ini adalah cara yang mudah untuk merekam API yang dirancang untuk kaki Anda sehingga harus didokumentasikan baik di mem :: uninitialized dan intrinsics :: uninit

Masalahnya adalah saat ini tidak ada cara yang benar untuk melakukan ini - itulah sebabnya kami bekerja keras untuk membuat MaybeUninit distabilkan sehingga fungsi-fungsi ini dapat diganti dokumentasinya dengan "JANGAN GUNAKAN" yang gemuk.


Diskusi seperti ini dan masalah seperti ini membuat saya semakin setuju dengan @japaric bahwa kita harus mengeluarkan sesuatu secepatnya. Pada dasarnya kita perlu ini dan ini daftar centang untuk mendapatkan berdetak, aku akan mengatakan. Kemudian kami memiliki cukup banyak untuk memberikan beberapa pola dasar.

Apakah mungkin untuk menstabilkan const yang tidak diinisialisasi, as_ptr dan
as_mut_ptr di depan API lainnya? Tampaknya sangat mungkin bagi saya bahwa ini
akan stabil seperti sekarang.

1 untuk ini. Akan sangat bagus jika fungsi ini tersedia di stable. Ini akan memungkinkan orang untuk bereksperimen dengan berbagai API tingkat yang lebih tinggi (dan berpotensi aman) selain API tingkat rendah dasar ini. Dan sepertinya aspek API ini cukup tidak kontroversial.

Selain itu, saya ingin menyarankan bahwa get_ref dan get_mut tidak pernah distabilkan, dan dihapus seluruhnya. Biasanya, bekerja dengan referensi lebih aman daripada bekerja dengan pointer mentah (dan dengan demikian orang mungkin tergoda untuk menggunakan metode ini lebih dari as_ptr dan as_mut_ptr meskipun mereka ditandai tidak aman), tetapi dalam kasus ini mereka benar-benar lebih berbahaya daripada metode penunjuk mentah karena dapat menyebabkan UB sedangkan metode penunjuk tidak bisa.

Jika aturannya adalah "jangan pernah membuat referensi ke memori yang tidak diinisialisasi" maka saya pikir kita harus membantu orang untuk mematuhi aturan ini dengan membuatnya hanya mungkin untuk membuat referensi seperti itu dengan melakukannya secara eksplisit, daripada memiliki metode pembantu yang melakukannya secara internal .

Dengan asumsi https://github.com/rust-lang/rfcs/pull/2582 , apakah kami sepenuhnya yakin bahwa (1) bahkan bukan UB meskipun (2) adalah, dan (1) juga berisi derefencing sebuah pointer yang menunjuk ke memori yang tidak diinisialisasi?

(1) unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).bar) as *mut bool, true); }
(2) let x: bool = mem::uninitialized();

Dan jika ya, apa logika dibalik itu (semoga kita bisa memasukkan sebagian pembahasan tentang masalah ini ke dalam dokumentasi untuk MaybeUninit)? Saya menebak sesuatu seperti karena dalam (1) nilai dereferensi selalu tetap menjadi "rvalue" dan tidak pernah menjadi dan "lvalue", sedangkan di (2) bool yang tidak valid menjadi "lvalue" dan dengan demikian sebenarnya harus diwujudkan dalam memori (Saya tidak yakin apa istilah yang benar untuk ini di Rust, tapi saya telah melihat istilah ini digunakan untuk C ++).

Dan apakah orang lain berpikir bahwa akan bermanfaat untuk membuat RFC untuk sintaks akses bidang pada pointer mentah yang mengevaluasi langsung ke pointer mentah ke bidang untuk menghindari kebingungan ini sejak awal?

Jika aturannya adalah "jangan pernah membuat referensi ke memori yang tidak diinisialisasi"

Saya tidak berpikir itu harus menjadi aturan, tetapi mungkin saja. Ini sedang dibahas sekarang di UCG.

apakah kita benar-benar yakin bahwa (1) bukan UB meskipun (2) adalah, dan (1) juga berisi derefencing penunjuk yang menunjuk ke memori yang tidak diinisialisasi?

Pertanyaan bagus! Tapi ya, pada dasarnya kita - di luar kebutuhan geser. Pikirkan &mut foo as *mut bool sebagai &raw mut foo , sebuah ekspresi atom dari tipe *mut bool . Tidak ada referensi di sini, hanya ptr mentah ke memori yang tidak diinisialisasi - dan itu tidak masalah.

let x: bool = mem::uninitialized();

Ini UB. Ini tidak ada hubungannya dengan referensi.

Asumsi implisit saya adalah bahwa rust-lang / rfcs # 2582 akan membuat contoh di atas valid dan terdefinisi dengan baik.

Saya sangat terkejut dengan ini. RFC itu hanya tentang referensi. Mengapa Anda menganggap itu mengubah sesuatu tentang boolean?

@RalfJung Saya kira saya pikir itu bukan UB karena nilai undefined tidak dapat diobservasi karena langsung ditimpa dengan nilai bool yang valid. Tapi saya rasa bukan itu masalahnya?

Untuk contoh yang lebih rumit, di mana nilai dalam x mengimplementasikan Drop, pointer mentah akan diperlukan untuk menimpa nilainya, dan itulah mengapa saya pikir rfc 2582 diperlukan untuk menghindari UB.

Saya kira saya mengira itu bukan UB karena nilai yang tidak ditentukan tidak dapat diobservasi karena segera ditimpa dengan nilai bool yang valid. Tapi saya rasa bukan itu masalahnya?

Hasil semantik pernyataan demi pernyataan (melihat MIR). Setiap pernyataan harus masuk akal. let x: bool = mem::uninitialized(); mewujudkan boolean yang buruk, dan tidak peduli apa yang terjadi nanti - Anda tidak boleh mewujudkan boolean yang buruk.

Saya memahami bahwa nilai x tidak valid, tetapi apakah itu memerlukan perilaku yang tidak ditentukan? Saya bisa melihat bagaimana ini bisa, secara umum, diambil di luar konteks. Tetapi dalam konteks contoh khusus itu, apakah perilaku tidak didefinisikan dengan baik? Saya kira masalah mendasar saya adalah saya tidak sepenuhnya memahami arti dari "perilaku tidak terdefinisi".

Kami ingin kompilator dapat mengandalkan invarian tertentu. Ini adalah invarian hanya jika mereka selalu bertahan. Begitu kami mulai menambahkan pengecualian, itu menjadi berantakan.

Mungkin Anda mengharapkan sesuatu yang lebih dari bentuk " memeriksa nilai membutuhkan validitas invariant untuk dipegang". Di sini, "memeriksa" bool akan menggunakannya dalam if . Itu spesifikasi yang masuk akal, tetapi kurang berguna: sekarang compiler harus membuktikan bahwa nilai sebenarnya "diperiksa" sebelum dapat mengasumsikan invariannya.

apakah itu membutuhkan perilaku yang tidak terdefinisi?

Kami memilih perilaku yang tidak terdefinisi dan apa adanya. Itu bagian dari mendesain bahasa. Perilaku undefined hampir tidak pernah "diperlukan" per se - tapi itu perlu untuk memungkinkan lebih banyak optimasi. Jadi seni di sini adalah untuk menemukan definisi dari perilaku tidak terdefinisi (yang bertentangan seperti yang mungkin terdengar ^^) yang memungkinkan pengoptimalan yang diinginkan, dan sesuai dengan harapan pemrogram (tidak aman).

Saya tidak sepenuhnya memahami arti dari "perilaku tidak terdefinisi".

Saya menulis posting blog tentang itu , tetapi jawaban singkatnya adalah bahwa perilaku yang

Apa yang sebenarnya tertulis dalam kontrak tergantung pada bahasa pemrograman. Tentu saja ada kendala (misalnya kita dibatasi oleh LLVM). Dalam kasus kami, UCG percaya (sesuai dengan apa yang kami dengar dari tim bahasa dan penyusun) bahwa kami ingin kontrak berisi klausa berikut: "Setiap kali nilai r dibuat, pemrogram harus membuktikan bahwa nilai r ini akan selalu memenuhi invarian validitas. " Tidak ada hukum fisika atau komputer yang memaksa kita untuk memiliki klausul ini dalam kontrak, tetapi ini dianggap sebagai kompromi yang masuk akal di antara banyak pilihan yang berbeda.

Secara khusus, kami sudah mengeluarkan informasi untuk LLVM yang tidak dapat kami kirimkan dengan kontrak yang lebih lemah. Kami dapat memutuskan untuk mengubah apa yang kami beri tahu LLVM, tentu saja - tetapi jika pilihannya adalah antara "kode tidak aman harus menggunakan MaybeUninit setiap kali berurusan dengan memori yang tidak diinisialisasi" dan " semua kode dapat dioptimalkan lebih sedikit", yang pertama tampaknya menyukai pilihan yang lebih baik.

Mengambil contoh Anda:

let x: bool = mem::uninitialized();

Kode ini UB di Rustc hari ini. Jika Anda melihat IR LLVM (tidak dioptimalkan) untuk mem::uninitialized::<bool>() , inilah yang Anda dapatkan:

; core::mem::uninitialized
; Function Attrs: inlinehint nonlazybind uwtable
define zeroext i1 @_ZN4core3mem13uninitialized17h6c99c480737239c2E() unnamed_addr #0 !dbg !5 {
start:
  %tmp_ret = alloca i8, align 1
  %0 = load i8, i8* %tmp_ret, align 1, !dbg !14, !range !15
  %1 = trunc i8 %0 to i1, !dbg !14
  br label %bb1, !dbg !14

bb1:                                              ; preds = %start
  ret i1 %1, !dbg !16
}
; snip
!15 = !{i8 0, i8 2}

Pada dasarnya, fungsi ini mengalokasikan 1 byte pada stack dan kemudian memuat byte tersebut. Namun beban ditandai dengan !range , yang memberi tahu LLVM bahwa byte harus antara 0 <= x <2, yaitu hanya bisa 0 atau 1. LLVM akan menganggap bahwa ini benar, dan perilaku tidak ditentukan jika batasan ini dilanggar.

Singkatnya, masalahnya bukan pada variabel yang tidak diinisialisasi itu sendiri, melainkan fakta bahwa Anda menyalin dan memindahkan nilai yang melanggar batasan tipenya.

Terima kasih atas eksposisinya! Jauh lebih jelas sekarang!

Saya kira masalah mendasar saya adalah saya tidak sepenuhnya memahami arti dari "perilaku tidak terdefinisi".

Rangkaian posting blog ini (yang memiliki contoh yang agak menarik / menakutkan di posting kedua) cukup membantu, menurut saya: http://blog.llvm.org/2011/05/what-every-c-programmer-should-know .html

Saya merasa ini sangat membutuhkan dokumentasi yang baik. Perubahan di sini mungkin merupakan hal yang baik karena beberapa alasan yang dapat saya daftar dan mungkin yang lainnya tidak. Tapi penggunaan yang benar dari memori yang tidak diinisialisasi (dan penggunaan tidak aman lainnya) bisa menjadi sangat kontra-intuitif. Nomicon memiliki bagian tentang uninitialized (yang mungkin akan diperbarui untuk membicarakan jenis ini), tetapi tampaknya tidak mengungkapkan seluruh kompleksitas masalah.

(Bukannya saya secara sukarela menulis dokumentasi semacam itu. Saya mencalonkan ... siapa pun yang tahu lebih banyak tentang ini daripada saya.)

Ide menarik dari https://github.com/rust-lang/rust/issues/55422#issuecomment -433943803: Kita bisa mengubah metode seperti into_inner menjadi fungsi, sehingga Anda harus menulis MaybeUninit::into_inner(foo) bukannya foo.into_inner() - yang mendokumentasikan dengan lebih jelas apa yang sedang terjadi.

Di https://github.com/rust-lang/rust/pull/58129 saya menambahkan beberapa dokumen, mengembalikan &mut T dari set dan mengganti nama into_inner menjadi into_initialized .

Saya pikir setelah ini, dan setelah https://github.com/rust-lang/rust/pull/56138 diselesaikan, kita dapat melanjutkan dengan menstabilkan bagian API (konstruktor, as_ptr , as_mut_ptr , set , into_initialized ).

Mengapa MaybeUninit::zeroed() a const fn ? ( MaybeUninit::uninitialized() adalah const fn )

EDIT: bisakah itu benar-benar dibuat const fn menggunakan Karat malam?

Mengapa MaybeUninit::zeroed() a const fn ? ( MaybeUninit::uninitialized() adalah const fn )

@gnzlbg Saya mencoba , tetapi membutuhkan salah satu dari yang berikut:

Satu hal yang paling membuat saya khawatir tentang pindah ke stabilisasi segera adalah kurangnya umpan balik dari orang-orang yang benar-benar menggunakan tipe ini. Tampaknya semua orang menunggu ini distabilkan sebelum mereka mulai menggunakannya. Itu adalah masalah, karena itu berarti kami akan terlambat memperhatikan masalah API.

@ rust-lang / libs apa kondisi biasa di mana Anda akan menggunakan fungsi sebagai ganti metode? Saya bertanya-tanya apakah beberapa operasi di sini harus berfungsi sehingga orang harus menulis, misalnya, MaybeUninit::as_ptr(...) . Saya khawatir ini akan meledakkan kode sehingga menjadi tidak dapat dibaca - tetapi OTOH, beberapa fungsi pada ManuallyDrop melakukan hal ini.

@RalfJung Pemahaman saya adalah bahwa metode dihindari pada hal-hal yang mengacu pada parameter umum, untuk menghindari metode tersembunyi dari tipe pengguna - maka ManuallyDrop::take .

Karena MaybeUninit<T> tidak akan pernah menjadi Deref<Target = T> , saya pikir metode yang sesuai di sini.

Mintalah umpan balik dan kamu akan menerima. Saya menggunakan MaybeUninit untuk mengimplementasikan fungsionalitas baru di std baru-baru ini.

  1. Di sys / sgx / ext / arch.rs saya menggunakannya dalam kombinasi dengan perakitan inline. Saya benar-benar menggunakan get_mut tidak benar, referensi pemikiran dan petunjuk mentah akan setara (diperbaiki di 928efca1). Saya sudah berada di blok yang tidak aman jadi saya tidak terlalu memperhatikan perbedaannya pada awalnya.
  2. Di sys / sgx / rwlock.rs , saya menggunakannya untuk memastikan pola bit dari const fn new() sama dengan penginisialisasi array di file header C. Saya menggunakan zeroed diikuti oleh set untuk mencoba memastikan bit "tidak peduli" adalah 0. Saya tidak tahu apakah ini penggunaan yang benar, tetapi tampaknya berfungsi dengan baik .
  1. Saya akan sangat bingung jika out.get_mut() as *mut _ ! = out.as_mut_ptr() . Tampak benar-benar C ++ ish. Saya berharap itu akan diperbaiki entah bagaimana.

Apa gunanya get_mut() ?

Satu hal yang saya ingin tahu baru-baru ini adalah apakah MaybeUninit<T> dijamin memiliki tata letak yang sama dengan T , dan apakah sesuatu seperti itu dapat digunakan untuk menginisialisasi sebagian nilai pada heap lalu mengubahnya menjadi sepenuhnya nilai yang diinisialisasi, misalnya sesuatu seperti ( taman bermain penuh )

struct Foo {
    x: i32,
}

let mut partial: Box<MaybeUninit<Foo>> = Box::new(MaybeUninit::uninitialized());
let complete: Box<Foo> = unsafe {
    ptr::write(&mut (*partial.as_mut_ptr()).x, 5);
    mem::transmute(partial)
};

menurut Miri contoh ini berfungsi (walaupun, sekarang saya menyadari bahwa saya tidak tahu apakah mentransmutasikan jenis kotak dengan tata letak yang identik itu sendiri terdengar).

@ Nemo157 mengapa Anda memerlukan tata letak memori yang sama ketika Anda memiliki into_inner ?

@Pzixel untuk menghindari penyalinan nilai setelah inisialisasi, bayangkan itu berisi buffer 100MB yang akan menyebabkan stack overflow jika dialokasikan pada stack. Meskipun, menulis kasus uji tampaknya ini membutuhkan API tambahan fn uninit_boxed<T>() -> Box<MaybeUninit<T>> untuk memungkinkan alokasi kotak yang tidak diinisialisasi tanpa menyentuh tumpukan.

Menggunakan sintaks box untuk memungkinkan pengalokasian ruang heap yang tidak diinisialisasi, Anda dapat melihat bahwa transmutasi seperti ini berfungsi, saat mencoba menggunakan into_initialized menyebabkan stack overflow: playground

@ Nemo157 Mungkin lebih baik menerapkan kompiler untuk mengoptimalkan penyalinan? Saya pikir itu harus melakukannya, tetapi mungkin ada atribut untuk memastikan kompilasi melakukannya.

@ Nemo

Satu hal yang saya ingin tahu baru-baru ini adalah apakah MaybeUninit<T> dijamin memiliki tata letak yang sama dengan T , dan apakah sesuatu seperti itu dapat digunakan untuk menginisialisasi sebagian nilai pada heap lalu mengubahnya menjadi sepenuhnya nilai yang diinisialisasi,

Saya percaya bahwa ini dijamin, dan bahwa Anda kode berlaku, dengan beberapa peringatan:

  • Bergantung pada jenis yang Anda gunakan (dan terutama dalam kode generik), Anda mungkin memerlukan ptr::write_unaligned .
  • Jika ada lebih banyak kolom, dan hanya beberapa di antaranya yang diinisialisasi, Anda tidak boleh mentransmutasikan ke T hingga semua kolom diinisialisasi sepenuhnya .

Ini juga kasus penggunaan yang saya minati, karena saya yakin ini dapat dikombinasikan dengan proc-macro untuk menyediakan abstraksi pembangun di tempat yang aman.

@Pzixel Jika memiliki tata letak memori yang sama maka Anda dapat menghindari menyalin seluruh struktur data setelah Anda membuatnya. Tentu saja kompilator dapat menghilangkan salinannya, dan itu mungkin tidak menjadi masalah untuk struktur kecil. Tapi itu pasti bagus untuk dimiliki.

@nicoburns ya, saya melihatnya sekarang. Saya hanya berbicara bahwa mungkin ada beberapa atribut, misalnya #[same_layout] atau #[elide_copying] , atau keduanya, atau sesuatu yang lain, untuk memastikan atribut berfungsi dengan cara yang sama seperti transmute . Atau mungkin mengubah implementasi into_constructed untuk menghindari penyalinan tambahan. Saya berharap ini menjadi perilaku default, tidak hanya untuk orang pintar yang membaca dokumen tentang tata letak. Maksud saya, saya memiliki kode yang memanggil into_constructed dan saya mendapatkan salinan tambahan, tetapi @ Nemo157 hanya memanggil transmute dan dia baik-baik saja. Tidak ada alasan mengapa into_constructed tidak dapat melakukan hal yang sama.

Saya akan sangat bingung jika out.get_mut() as *mut _ ! = out.as_mut_ptr() . Tampak benar-benar C ++ ish. Saya berharap itu akan diperbaiki entah bagaimana.

Apa gunanya get_mut() ?

Saya membuat poin serupa di atas bahwa get_mut() dan get_ref() berpotensi membingungkan / membuatnya mudah untuk secara tidak sengaja memunculkan perilaku yang tidak ditentukan (karena mereka memberikan ilusi sebagai alternatif yang lebih aman untuk as_ptr() dan as_mut_ptr() , tetapi kenyataannya kurang aman dibandingkan metode-metode tersebut).

Saya yakin mereka tidak berada dalam subset API yang diusulkan @RalfJung untuk stabilisasi (lihat: https://www.ralfj.de/blog/2019/02/12/all-hands-recap.html)

@RalfJung Mengenai proposal Anda untuk metode ptr::freeze() (https://www.ralfj.de/blog/2019/02/12/all-hands-recap.html):

Apakah masuk akal untuk memiliki metode serupa untuk membangun MaybeUninit ? ( MaybeUninit::frozen() , MaybeUninit::abitrary() atau serupa). Secara intuitif, tampaknya memori tersebut akan sama performanya dengan memori yang benar-benar tidak diinisialisasi untuk banyak kasus penggunaan, tanpa biaya penulisan ke memori seperti zeroed . Mungkin bahkan dapat direkomendasikan melalui konstruktor uninitialized kecuali jika orang benar-benar yakin bahwa mereka memerlukan memori yang tidak diinisialisasi?

Pada catatan itu, apa kasus penggunaan di mana Anda benar-benar membutuhkan memori yang "tidak diinisialisasi" daripada memori yang "dibekukan"?

@Tokopedia

1. I'd be very confused if `out.get_mut() as *mut _` != `out.as_mut_ptr()`. Looks really C++ish. I hope it would be fixed somehow.

Dicatat. Alasan mengapa beberapa orang mengusulkan hal ini adalah karena akan berguna untuk menyatakan &mut ! tidak berpenghuni (seperti dalam, memiliki nilai seperti itu adalah UB). Namun, dengan MaybeUninit::<!>::uninitiailized().get_mut() , kami telah membuat nilai seperti itu. Itulah mengapa as_mut_ptr kurang berbahaya - ini menghindari pembuatan referensi.

@nicoburns (Perhatikan bahwa freeze bukan ide saya, saya hanya menjadi bagian dari diskusi dan saya sangat menyukai proposalnya.)

Saya yakin mereka _not_ dalam subset API yang diusulkan @RalfJung untuk distabilkan

Benar. Dan memang mungkin kita seharusnya tidak memilikinya sama sekali.

Apakah masuk akal untuk memiliki metode serupa untuk membangun MaybeUninit ? ( MaybeUninit::frozen() , MaybeUninit::abitrary() atau serupa).

Iya! Saya akan mengusulkan untuk menambahkan ini setelah MaybeUninit stabil dan ptr::freeze telah mendarat.

Pada catatan itu, apa kasus penggunaan di mana Anda benar-benar membutuhkan memori yang "tidak diinisialisasi" daripada memori yang "dibekukan"?

Ini perlu lebih banyak belajar dan benchmarking, harapannya mungkin cost performance karena LLVM tidak akan melakukan optimasi yang seharusnya bisa dilakukan.

(Saya akan kembali ke komentar lain juga, begitu saya punya waktu.)

@Pzixel dapat membuat objek langsung ke dalam memori yang dialokasikan sebelumnya adalah hal yang tidak sepele, Rust memiliki dua RFC yang diterima untuk mengimplementasikan hal seperti itu (lebih dari 4 tahun yang lalu!), Tetapi sejak itu mereka tidak diterima dan sebagian besar implementasi dihapus (kecuali sintaks box saya gunakan di atas). Jika Anda menginginkan detail lebih lanjut , utas i.rl.o tentang penghapusan akan menjadi tempat terbaik untuk memulai.

Seperti @nicoburns menyebutkan MaybeUninit berpotensi digunakan sebagai blok penyusun untuk solusi berbasis perpustakaan yang kurang ergonomis untuk masalah yang sama, sangat berguna sebagai cara untuk mulai bereksperimen dengan konsep dan melihat jenis API apa yang digunakan. memungkinkan pembangunan. Itu hanya tergantung pada apakah MaybeUninit dapat memberikan jaminan yang diperlukan untuk membangun solusi semacam itu.

@ Nemo157 Saya hanya menyarankan untuk menggunakannya di satu tempat, tidak ada yang berurusan dengan kasus umum non-sepele.

@jethrogb Terima kasih banyak! Jadi sepertinya API berfungsi dengan baik untuk Anda saat ini?

2. Di sys / sgx / rwlock.rs , saya menggunakannya untuk memastikan pola bit dari const fn new() sama dengan penginisialisasi array di file header C.

Woah, itu gila. ^^ Tapi saya rasa itu akan berhasil, itu adalah const fn tanpa argumen sama sekali jadi harus selalu mengembalikan hal yang sama ...

Satu hal yang saya ingin tahu baru-baru ini adalah apakah MaybeUninit<T> dijamin memiliki tata letak yang sama dengan T , dan apakah sesuatu seperti itu dapat digunakan untuk menginisialisasi sebagian nilai pada heap lalu mengubahnya menjadi sepenuhnya nilai yang diinisialisasi

Pada daftar hal-hal yang harus kita tambahkan pada akhirnya adalah sesuatu seperti

fn into_initialized_box(Box<MaybeUninit<T>>) -> Box<T>

yang mengubah Box .

Tapi ya, saya pikir kita harus mengizinkan transmutasi seperti itu. Apakah ada preseden untuk mengatakan di dokumen "Anda dapat mengubah ini dengan cara berikut"? Saya pikir biasanya kami lebih suka menambahkan metode pembantu daripada orang melakukan transmutasi mereka sendiri.

  • Bergantung pada jenis yang Anda gunakan (dan terutama dalam kode generik), Anda mungkin memerlukan ptr::write_unaligned .

Dalam kode umum Anda tidak dapat mengakses bidang. Saya pikir jika Anda dapat mengakses bidang Anda biasanya tahu jika struct dikemas, dan jika tidak maka ptr::write sudah cukup baik. (Jangan gunakan tugas karena itu mungkin turun! Saya terus lupa itu ...)

Meskipun, menulis kasus uji tampaknya ini membutuhkan API tambahan fn uninit_boxed<T>() -> Box<MaybeUninit<T>> untuk memungkinkan alokasi kotak yang tidak diinisialisasi tanpa menyentuh tumpukan.

Itu bug , tetapi karena bug itu mungkin sulit untuk diperbaiki, mungkin ada baiknya juga menawarkan konstruktor terpisah untuk ini. Tidak yakin bagaimana cara menerapkannya. Dan kemudian kita mungkin juga menginginkan sesuatu seperti zeroed_box yang menghindari zeroeing slot tumpukan dan kemudian memcpying, dan seterusnya ... Saya tidak suka semua duplikasi ini. : /

Jadi saya akan mengusulkan bahwa setelah / secara paralel dengan stabilisasi awal, beberapa orang yang memiliki kasus penggunaan untuk memor yang tidak diinisialisasi di heap (pada dasarnya, mencampur Box dan MaybeUninit ) berkumpul dan merancang minimal kemungkinan ekstensi API untuk itu. @eddyb juga mengungkapkan minatnya terhadap hal ini. Itu tidak benar-benar terkait dengan hanya mencela mem::uninitialized lagi, jadi saya pikir itu harus mendapatkan tempat sendiri untuk diskusi, di luar masalah pelacakan (cara-terlalu-besar-sudah) ini.

Sedikit tanggapan saya: Saya biasanya senang dengan MaybeUninit<T> . Saya tidak punya keluhan besar. Ini kurang dari footgun dari mem::uninitialized , yang bagus. Metode const new dan uninitialized bagus. Saya berharap lebih banyak metode yang konstan, tetapi seperti yang saya pahami, banyak dari mereka memerlukan lebih banyak kemajuan untuk dibuat pada const fn secara umum sebelum mereka dapat dibuat const .

Saya ingin jaminan yang lebih kuat daripada "tata letak yang sama" untuk T dan MaybeUninit<T> . Saya ingin mereka kompatibel dengan ABI (efektif, #[repr(transparent)] , meskipun saya tahu bahwa atribut tidak dapat diterapkan ke serikat pekerja) dan aman FFI (yaitu, jika T aman untuk FFI , maka MaybeUninit<T> seharusnya juga aman untuk FFI). (Secara tangensial, saya berharap kami dapat menggunakan #[repr(transparent)] pada serikat pekerja yang hanya memiliki satu bidang berukuran positif (seperti yang kami bisa untuk struct))

Saya sebenarnya mengandalkan ABI MaybeUninit<T> dalam proyek saya untuk membantu pengoptimalan (tetapi tidak dengan cara yang tidak aman, jadi jangan panik). Saya senang menjelaskan secara detail jika ada yang tertarik, tetapi saya akan membuat komentar ini tetap singkat dan menghilangkan detailnya untuk saat ini.

@mjbaw Terima kasih!

Saya berharap kita bisa menggunakan #[repr(transparent)] pada serikat pekerja yang hanya memiliki satu bidang berukuran positif (seperti yang kami bisa untuk struct).

Setelah atribut itu ada, menambahkannya ke MaybeUninit tidak perlu dipikirkan lagi. Dan sebenarnya logika untuk ini telah dilaksanakan di rustc ( MaybeUninit<T> de-facto adalah ABI-kompatibel dengan T , tapi kami tidak menjamin itu.)

Yang diperlukan seseorang hanyalah menulis RFC dan melihatnya, dan menambahkan beberapa pemeriksaan yang memastikan repr(transparent) unions hanya memiliki satu bidang non-ZST. Apakah Anda ingin mencobanya? : D

Yang diperlukan seseorang adalah menulis RFC dan melihatnya, dan menambahkan beberapa pemeriksaan yang memastikan repr(transparent) unions hanya memiliki satu bidang non-ZST. Apakah Anda ingin mencobanya? : D

@RalfJung Tanyakan dan kamu akan menerima!

Cc https://github.com/rust-lang/rust/pull/58468

Ini hanya menyisakan API yang menurut saya cukup stabil di maybe_uninit , dan memindahkan sisanya ke dalam gerbang fitur terpisah.

Oke, PR persiapan semuanya sudah ada, dan into_inner juga hilang.

Namun, saya sangat ingin https://github.com/rust-lang/rfcs/pull/2582 diterima sebelum menstabilkan, jika tidak, kami bahkan tidak memiliki cara untuk menginisialisasi struct bidang demi bidang - dan sepertinya kasus penggunaan utama untuk MaybeUninit . Kami sangat dekat untuk memiliki semua kotak yang diperlukan agar FCP dapat dimulai.

Saya baru saja mengonversi kode saya untuk menggunakan MaybeUninit . Ada beberapa tempat di mana saya bisa menggunakan metode take yang bekerja pada &mut self daripada self . Saat ini saya menggunakan x.as_ptr().read() tetapi saya merasa bahwa x.take() atau x.take_initialized() akan jauh lebih jelas.

@Amanieu Ini terasa sangat mirip dengan metode into_inner yang ada. Mungkin kita bisa mencoba menghindari duplikasi di sini?

😉

Metode take dari Option memiliki semantik lain. x.as_ptr().read() tidak mengubah nilai dalam x, tapi Option::take coba ganti nilai. Ini mungkin menyesatkan bagi saya.

@ qwerty19106 x.as_ptr().read() pada MaybeUninit _semantically_ mengeluarkan nilai dan membiarkan pembungkusnya tidak diinisialisasi lagi, itu hanya terjadi bahwa nilai yang tidak diinisialisasi tertinggal memiliki pola bit yang sama dengan nilai yang diambil .

Saat ini saya menggunakan x.as_ptr().read() tetapi saya merasa bahwa x.take() atau x.take_initialized() akan jauh lebih jelas.

Saya merasa penasaran, bisakah Anda menjelaskan mengapa?

Dalam pandangan saya, metode mirip take agak menyesatkan karena tidak seperti take dan into_initialized , metode ini tidak melindungi dari pengambilan dua kali. Sebenarnya, untuk Copy jenis (dan pada kenyataannya untuk Copy nilai seperti None as Option<Box<T>> ), mengambil dua kali tidak masalah! Jadi, analogi dengan take tidak benar-benar berlaku, dari sudut pandang saya.

Kita bisa menyebutnya read_initialized() , tetapi pada saat itu saya benar-benar bertanya-tanya apakah itu memang lebih jelas dari as_ptr().read() .

x.as_ptr().read() pada MaybeUninit _semantically_ mengeluarkan vale dan membiarkan pembungkusnya tidak diinisialisasi lagi, itu hanya terjadi bahwa nilai yang tidak diinisialisasi tertinggal memiliki pola bit yang sama dengan nilai yang dikeluarkan.

MaybeUninit tidak benar-benar memiliki invarian semantik yang berguna, jadi saya tidak yakin saya sepenuhnya setuju dengan pernyataan itu. TBH Saya tidak yakin bahwa ada gunanya mempertimbangkan operasi pada MaybeUninit dengan cara lain selain hanya efek operasional mentahnya.

@RalfJung hmm, mungkin "semantik" kata yang salah disini. Dalam hal bagaimana pengguna harus menggunakan tipe tersebut, Anda harus mengasumsikan nilainya tidak diinisialisasi lagi setelah Anda membacanya (kecuali Anda secara konkret mengetahui bahwa tipe tersebut adalah Copy ).

Jika Anda hanya melihat efek operasional mentah Anda mendapatkan interaksi aneh seperti ini di mana Anda dapat melanggar invarian keamanan dari API tidak aman lainnya tanpa secara teknis membaca memori yang tidak diinisialisasi. (Saya agak berharap bahwa Miri masih akan melacak 0 panjang pembacaan memori yang tidak diinisialisasi, tetapi tampaknya tidak demikian).

@RalfJung Dalam semua kasus saya, ini melibatkan static mut mana nilai ditempatkan, dan kemudian diambil. Karena saya tidak dapat menggunakan statis, saya tidak dapat menggunakan into_uninitialized .

@Amanieu yang saya tanyakan adalah, menurut Anda mengapa x.take_initialized() lebih jelas dari x.as_ptr().read() ?

@ Nemo

Saya agak berharap bahwa Miri masih akan melacak 0 panjang pembacaan memori yang tidak diinisialisasi, tetapi tampaknya tidak demikian

Pembacaan 0-panjang memori yang tidak diinisialisasi tidak pernah UB, jadi mengapa Miri peduli tentang mereka?

Jika Anda hanya melihat efek operasional mentah Anda mendapatkan interaksi aneh seperti ini di mana Anda dapat melanggar invarian keamanan dari API tidak aman lainnya tanpa secara teknis membaca memori yang tidak diinisialisasi.

Tentu, Anda dapat melanggar invarian keamanan tanpa pernah membaca memori yang tidak diinisialisasi. Anda juga bisa menggunakan MaybeUninit::zeroed().into_initialized() untuk itu. Saya tidak melihat masalahnya.
"Interaksi aneh" di sini adalah Anda membuat dua nilai dari jenis yang tidak berhak Anda buat. Ini semua tentang invarian keamanan Spartacus , dan tidak ada hubungannya dengan invarian validitas.

Inilah sebabnya saya pikir read_initialized() menyampaikan lebih baik apa yang terjadi: Kami membaca data, dan kami mengklaim itu diinisialisasi dengan benar (yang termasuk memastikan kami benar-benar diizinkan untuk membuat nilai ini pada jenis itu). Ini tidak berpengaruh pada pola bit yang masih disimpan di MaybeUninit .

@RalfJung Pada dasarnya saya memperlakukan MaybeUninit sebagai Option , tetapi tanpa tag. Sebenarnya, saya sebelumnya menggunakan peti opsi tak bertag untuk tujuan ini, dan memiliki metode take untuk mengekstrak nilai dari serikat pekerja.

@Amanieu @shepmaster Saya menambahkan read_initialized di https://github.com/rust-lang/rust/pull/58660. Saya masih berpikir itu adalah nama yang lebih baik daripada take_initialized . Apakah ini memenuhi kebutuhan Anda?

Humas itu juga menambahkan contoh ke beberapa metode lain, umpan balik diterima!

Saya senang dengan read_initialized .

Sementara saya melakukannya, saya juga menghasilkan MaybeUninit<T>: Copy jika T: Copy . Sepertinya tidak ada alasan bagus untuk tidak melakukan itu.

Hm, mungkin get_initialized akan menjadi nama yang lebih baik? Ini semacam melengkapi set , setelah semua.

Atau mungkin set harus diganti namanya menjadi write ? Itu juga akan mencapai konsistensi.

Saya telah mengubah kode saya untuk menggunakan MaybeUninit dan menemukan bahwa bekerja dengan irisan yang tidak diinisialisasi sangat tidak ergonomis. Saya pikir ini dapat ditingkatkan jika kita memiliki fungsi untuk berikut ini:

  • Konversi aman dari &mut [T] menjadi &mut [MaybeUninit<T>] . Ini secara efektif memungkinkan parameter &out ditiru menggunakan &mut [MaybeUninit<T>] , yang berguna misalnya untuk read .
  • Konversi tidak aman dari &mut [MaybeUninit<T>] menjadi &mut [T] (dan hal yang sama untuk &[T] ), untuk digunakan setelah kita memanggil .set pada setiap elemen potongan.

API yang saya lihat terlihat seperti ini:

// The returned slice is truncated to the number of elements actually read.
fn read<T>(out: &mut [MaybeUninit<T>]) -> Result<&mut [T]>;

Saya setuju bahwa bekerja dengan irisan adalah tidak ergonomis saat ini, dan itulah sebabnya saya menambahkan first_ptr dan first_ptr_mut . Tapi itu mungkin jauh dari API terbaik.

Namun, saya lebih suka jika kita bisa berkonsentrasi pada mendapatkan "API inti" dikirim pertama, dan kemudian melihat interaksi dengan irisan (dan dengan Box ).

Saya suka ide untuk mengganti nama set menjadi write , memberikan konsistensi dengan ptr::write .

Dalam nada yang sama, apakah read_initialized benar-benar lebih baik daripada hanya read ? Jika kekhawatirannya adalah tentang penggunaan tidak disengaja yang menjadi tersembunyi, mungkin menjadikannya sebagai fungsi daripada metode, yaitu MaybeUninit::read(&mut v) ? Hal yang sama dapat dilakukan untuk write , yaitu MaybeUninit::write(&mut v) untuk konsistensi. Pengorbanan dalam kedua kasus tersebut adalah antara kegunaan dan ketelitian, dan jika ketelitian dianggap lebih baik dalam satu kasus, saya tidak melihat mengapa itu akan berbeda di kasus lain.

Terlepas dari itu, sampai API ini diselesaikan, saya sangat mendukung stabilisasi dengan API minimal, yaitu new , uninitialized , zeroed , as_ptr , as_mut_ptr , dan mungkin get_ref dan get_mut .

dan mungkin get_ref dan get_mut .

Ini seharusnya hanya distabilkan setelah kami menyelesaikan https://github.com/rust-rfcs/unsafe-code-guidelines/issues/77 , dan sepertinya perlu beberapa saat ...

menstabilkan dengan API minimal, yaitu new , uninitialized , zeroed , as_ptr , as_mut_ptr

Paket saya adalah into_initialized , set / write , dan read_initialized untuk menjadi bagian dari set minimal itu. Tapi mungkin tidak? set / write dan read_initialized dapat dengan mudah diimplementasikan dengan yang lainnya, jadi saya sekarang juga cenderung untuk tidak menstabilkannya di batch pertama. Tetapi memiliki sesuatu seperti into_initialized dari awal diinginkan, IMO.

mungkin menjadikannya sebuah fungsi daripada sebuah metode, yaitu MaybeUninit::read(&mut v) ? Hal yang sama dapat dilakukan untuk write , yaitu MaybeUninit::write(&mut v) untuk konsistensi.

Dari apa yang telah dibahas di sini sebelumnya, kami hanya menggunakan pendekatan fungsi eksplisit untuk menghindari masalah dengan Deref instance. Saya tidak berpikir kita harus memprioritaskan alasan lain untuk menggunakan fungsi daripada metode.

apakah read_initialized benar-benar lebih baik daripada hanya read ?

Pertanyaan bagus! Saya tidak tahu. Ini untuk simetri dengan into_initialized . Tetapi into_inner adalah metode umum di mana orang mungkin kehilangan gambaran umum tentang jenis namanya, read jauh lebih jarang. Dan mungkin seharusnya initialized bukannya into_initialized ? Begitu banyak pilihan ...

Dari apa yang telah dibahas di sini sebelumnya, kami hanya menggunakan pendekatan fungsi eksplisit untuk menghindari masalah dengan Deref instance. Saya tidak berpikir kita harus memprioritaskan alasan lain untuk menggunakan fungsi daripada metode.

Kecuali ptr::read dan ptr::write adalah fungsi, bukan metode. Jadi prioritas sudah ditetapkan untuk MaybeUninit::read dan MaybeUninit::write .

Sunting : Oke, ternyata ada metode read dan write pada pointer, juga ... Tidak pernah memperhatikan yang sebelumnya ... Tapi mereka menggunakan pointer, yang tidak masuk akal untuk MaybeUninit .

Begitu banyak pilihan ...

Sepakat. Sampai ada lebih banyak sepeda-shedding di metode lain, saya pikir hanya new , uninitialized , zeroed , as_ptr , as_mut_ptr benar-benar siap untuk stabilisasi.

Kecuali ptr::read dan ptr::write adalah fungsi, bukan metode. Jadi prioritasnya sudah ditetapkan

Mereka bukan bagian dari struktur data, tentu saja mereka adalah fungsi yang berdiri sendiri. Dan seperti yang Anda katakan, mereka saat ini ada sebagai metode juga.

Tapi mereka mengkonsumsi penunjuk

Petunjuk mentah adalah Copy , jadi tidak ada yang benar-benar dikonsumsi.

Petunjuk mentah adalah Copy , jadi tidak ada yang benar-benar dikonsumsi.

Poin bagus ...

Nah, v.as_ptr().read() sudah cukup ringkas dan jelas. as_ptr diikuti oleh read harus membuatnya menonjol sebagai sesuatu untuk dipikirkan dengan hati-hati, lebih dari into_initialized . Secara pribadi, saya mendukung hanya mengekspos as_ptr dan as_mut_ptr , setidaknya untuk saat ini. Dan, tentu saja, new , uninitialized , dan zeroed .

@Amanieu Bagaimana dengan sesuatu yang lebih seperti apa yang Cell , di mana terdapat konversi yang aman untuk &mut MaybeUninit<[T]> ke dan dari &mut [MaybeUninit<T>] ?

Itu akan memungkinkan hal berikut, yang tampaknya cukup alami bagi saya:

fn read<T>(out: &mut MaybeUninit<[T]>) -> Result<&mut [T]> {
    let split = out.as_mut_slice_of_uninit();
    // ... operate on split ...
    return Some(unsafe { split[0..n].as_uninit_mut_slice().get_mut() })
}

Ini juga terasa lebih akurat mewakili semantik ke pemanggil. Fungsi mengambil &mut [MaybeUninit<T>] akan terasa, bagi saya, seperti itu mungkin memiliki beberapa logika pembeda yang mana yang baik dan mana yang tidak. Mengambil &mut MaybeUninit<[T]> , di sisi lain menyatakan bahwa itu tidak akan membedakan antara sel ketika sampai pada data apa yang sudah ada di dalamnya.

(Nama-nama metodenya, tentu saja, tunduk pada bikeshedding - saya hanya meniru apa yang dilakukan Cell .)

@eternaleye MaybeUninit<[T]> bukan tipe yang valid karena serikat tidak boleh DST.

Mm, benar

Sampai ada lebih banyak sepeda-shedding di metode lain, saya pikir hanya new , uninitialized , zeroed , as_ptr , as_mut_ptr benar-benar siap untuk stabilisasi.

Yah, saya pikir kita harus menerima RFC ini sebelum menstabilkan apa pun - jika tidak, kita bahkan tidak memiliki cara yang dikenai sanksi untuk menginisialisasi struct bidang demi bidang, yang tampaknya seperti minimum.

Jadi sementara kita menunggu percobaan , kita bisa bikeshed sedikit tentang nama untuk apa yang sekarang disebut set , read_initialized dan into_initialized . Penggantian nama berikut telah disarankan:

  1. set -> write . Metafora terbaik untuk .as_ptr().read() tampaknya "membaca", bukan "mendapatkan", tetapi pelengkap ( .as_ptr_mut().write() ) harus "tulis", bukan "set".
  2. read_initialized -> read . Cocok dengan write , tetapi tidak aman. Apakah itu (plus dokumentasi) cukup sebagai peringatan sehingga Anda harus secara manual memastikan bahwa data sudah diinisialisasi? Ada banyak kesepakatan bahwa into_inner yang tidak aman tidaklah cukup, itulah sebabnya saya mengganti namanya menjadi into_initialized .
  3. into_initialized -> initialized . Jika kita memiliki read_initialized dan into_initialized , itu memiliki konsistensi yang bagus untuk itu IMO - tetapi jika read , maka into_initialized menonjol sedikit. Nama metodenya cukup panjang. Namun, operasi yang paling memakan waktu disebut into_* , dari apa yang saya tahu.

Adakah keberatan untuk (1)? Dan saya kebanyakan bersandar pada (3). Untuk (2) Saya ragu-ragu: read lebih mudah untuk diketik, tetapi read_initialized IMO bekerja lebih baik ketika membaca kode tersebut - dan kode lebih sering dibaca dan ditinjau daripada ditulis. Sepertinya menyenangkan untuk menyebutkan tempat di mana kita benar-benar menganggap hal-hal akan diinisialisasi.

Pikiran, opini?

Yah, saya pikir kita harus menerima RFC ini sebelum menstabilkan apa pun - jika tidak, kita bahkan tidak memiliki cara yang dikenai sanksi untuk menginisialisasi struct bidang demi bidang, yang tampaknya seperti minimum.

Apakah ini tempat saya memasang steker untuk offset_of! ? :)

Perhatikan bahwa read_initialized adalah superset ketat dari into_initialized (mengambil &self bukan self ). Apakah masuk akal untuk mendukung keduanya?

Apakah ini tempat saya memasang steker untuk offset_of! ? :)

Jika Anda bisa membuatnya stabil sebelum RFC saya diterima, tentu. ;)

Apakah masuk akal untuk mendukung keduanya?

IMO ya. into_initialized lebih aman karena mencegah penggunaan nilai yang sama dua kali, dan karenanya sebaiknya lebih disukai daripada read_initialized bila memungkinkan.

Jadi @nikomatsakis telah membuat poin ini sebelumnya, tetapi tidak menjadikannya sebagai pemblokir yang sulit.

Saya baru saja mem-port banyak kode untuk menggunakan MaybeUninit<T> dan into_initialized dan saya merasa itu tidak perlu bertele-tele. Kode sudah jauh lebih bertele-tele daripada sebelumnya di mana "salah" menggunakan mem::uninitialized .

Saya pikir MaybeUninit<T> seharusnya hanya disebut Uninit<T> , karena untuk semua tujuan praktis, jika Anda mendapatkan MaybeUninit<T> tidak diketahui Anda harus berasumsi bahwa itu tidak diinisialisasi, jadi Uninit<T> akan meringkasnya dengan benar. Juga, into_uninitialized seharusnya hanya into_init() atau serupa untuk alasan konsistensi.

Kita juga bisa memanggil tipe Uninitialized<T> dan metode into_initialized , tetapi menggunakan singkatan untuk tipe dan bentuk panjang untuk metode atau sebaliknya adalah ketidakkonsistenan yang menyakitkan. Idealnya saya hanya perlu mengingat bahwa "API Rust menggunakan singkatan / bentuk panjang" dan hanya itu.

Karena singkatan bisa jadi ambigu bagi orang yang berbeda, saya lebih suka menggunakan bentuk panjang di mana saja dan menyebutnya sehari. Tetapi menggunakan campuran adalah IMO yang terburuk dari kedua dunia. Rust cenderung menggunakan singkatan lebih sering daripada bentuk yang lebih panjang, jadi saya tidak akan menentang Uninit<T> sebagai singkatan dan .into_init() sebagai singkatan lain untuk metode ini.

Saya tidak suka into_initialized() , karena sepertinya ada transformasi yang terjadi untuk menginisialisasi nilainya. Saya lebih suka take_initialized() . Saya menyadari jenis tanda tangan berangkat dari metode take , tetapi menurut saya ini jauh lebih jelas, secara semantik, dan saya percaya kejelasan semantik harus menggantikan konsistensi peminjaman / pemindahan. Alternatif lain yang belum didahulukan sebagai pinjaman yang bisa berubah bisa jadi move_initialized atau consume_initialized .

Adapun set() vs write() , saya sangat menyukai write() untuk memanggil kemiripan dengan as_ptr().write() , yang akan menjadi alias.

Dan akhirnya, jika akan ada take_initialized() atau yang serupa, maka saya memilih read_initialized() lebih dari read() karena penjelasan yang jelas dari yang pertama.

Sunting : tetapi untuk memperjelas, saya pikir bertahan dengan as_ptr().write() dan as_ptr().read() bahkan lebih jelas dan lebih mungkin memicu sirkuit mental BAHAYA BAHAYA .

@gnzlbg kami memiliki FCP untuk nama jenisnya, saya tidak yakin apakah kami harus membuka kembali diskusi itu.

Namun, saya menyukai usulan untuk menggunakan "init" secara konsisten, seperti pada MaybeUninit::uninit() dan x.into_init() .

Saya tidak suka into_initialized() , karena kedengarannya seperti sedang terjadi transformasi untuk menginisialisasi nilainya.

into metode sering tidak benar-benar melakukan transformasi apa pun selain melihat data yang sama (dimiliki) pada tipe tertentu - lihat misalnya berbagai metode into_vec .

Saya baik-baik saja dengan take_initialized(&mut self) (selain into_init), tetapi saya pikir itu harus mengembalikan status internal kembali ke undef .

mengembalikan keadaan internal kembali

https://github.com/rust-lang/rust/issues/53491#issuecomment -437811282

ini seharusnya tidak mengubah konten self sama sekali. Kepemilikan yang adil dipindahkan sehingga sekarang secara efektif dalam keadaan yang sama seperti ketika dibangun tanpa diinisialisasi.

Banyak dari hal-hal ini telah dibahas di 200+ komentar tersembunyi.

Banyak dari hal-hal ini telah dibahas di 200+ komentar tersembunyi.

Saya telah mengikuti diskusi ini beberapa lama, dan saya mungkin salah, tetapi jangan berpikir bahwa hal ini telah dikemukakan sebelumnya. Secara khusus, komentar yang Anda kutip tidak menyarankan untuk "mengembalikan keadaan internal kembali ke undef ", tetapi membuatnya setara dengan ptr::read (yang berarti membiarkan keadaan internal tidak berubah). Apa yang saya sarankan adalah konseptual yang setara dengan mem::replace(self, MaybeUninit::uninitialized()) .

ekuivalen konseptual dari mem::replace(self, MaybeUninit::uninitialized()) .

Karena arti dari undef , itu setara dengan read : https://rust.godbolt.org/z/e0-Gyu

@scottcm tidak. Dengan read , berikut ini legal:

let mut x = MaybeUninit::<u32>::uninitialized();
x.set(13);
let x1 = unsafe { x.read_initialized() };
// `u32` is `Copy`, so we may read multiple times.
let x2 = unsafe { x.read_initialized() };
assert_eq!(x1, x2);

Dengan usulan take , ini akan ilegal karena x2 akan menjadi undef .

Hanya karena dua fungsi menghasilkan rakitan yang sama tidak berarti keduanya setara.

Namun, saya tidak melihat manfaat dari menimpa konten dengan undef . Ini hanya memperkenalkan lebih banyak cara bagi orang untuk menembak diri sendiri. @jethrogb Anda belum memberikan motivasi apa pun, dapatkah Anda menjelaskan mengapa menurut Anda ini adalah ide yang bagus?

Saya baik-baik saja dengan take_initialized(&mut self) (selain into_init), tetapi saya pikir itu harus mengembalikan status internal kembali ke undef .

Saya mengusulkan take_initialized(self) alih-alih into_initialized(self) , karena saya yakin nama sebelumnya lebih tepat menggambarkan operasi. Sekali lagi, saya memahami bahwa take biasanya membutuhkan &mut self dan into biasanya membutuhkan self , tetapi saya yakin penamaan yang akurat secara semantik lebih penting daripada mengetik secara konsisten penamaan. Mungkin nama yang berbeda harus digunakan, seperti move_initialized atau transmute_initialized .

Dan, sekali lagi, untuk v.write() dan v.read_initialized() , saya tidak melihat nilai positif di atas v.as_ptr().write() dan v.as_ptr().read() . Dua yang terakhir tampaknya lebih kecil kemungkinannya untuk disalahgunakan.

Dan, sekali lagi, untuk v.write() dan v.read_initialized() , saya tidak melihat nilai positif di atas v.as_ptr().write() dan v.as_ptr().read() . Dua yang terakhir tampaknya lebih kecil kemungkinannya untuk disalahgunakan.

v.write() (atau v.set() atau apa pun yang kami sebut hari ini) aman. v.as_ptr().write() membutuhkan blok unsafe , yang agak mengganggu. Meskipun saya setuju tentang v.read_init() vs v.as_ptr().read() . v.read_init() sepertinya tidak berguna.

Saya mengusulkan take_initialized (self) daripada into_initialized (self), karena saya yakin nama sebelumnya lebih tepat menggambarkan operasi tersebut. Sekali lagi, saya memahami bahwa take biasanya mengambil & mut self dan menjadi biasanya mengambil self, tapi saya percaya penamaan yang akurat secara semantik lebih penting daripada penamaan yang diketik secara konsisten.

Saya sangat merasa bahwa into_init(ialized) juga secara semantik lebih akurat di sini - lagipula ia menghabiskan MaybeUninit .

@mjbshaw Ah, ya, memang begitu. Saya tidak memperhatikan bahwa ... Oke, baiklah, dalam hal itu saya mencabut semua komentar saya sebelumnya tentang set / write . Mungkin set lebih masuk akal; Cell dan Pin sudah menentukan metode set . Perbedaan utama adalah bahwa MaybeUninit::set tidak akan menjatuhkan nilai yang disimpan sebelumnya; mungkin itu masih mendekati write ... Entahlah. Bagaimanapun, dokumentasinya cukup jelas.

@RalfJung Oke, lupakan take... begitu. Bagaimana dengan nama baru, seperti move... , consume... , atau transmute... atau semacamnya? Saya pikir into_init(ialized) terlalu membingungkan; juga saya, itu menyiratkan nilai sedang diinisialisasi, ketika sebenarnya kami secara implisit menyatakan bahwa itu sudah diinisialisasi.

padahal sebenarnya kami secara implisit menyatakan bahwa itu sudah diinisialisasi.

Saya pikir ada baiknya untuk menyebutkan lagi bahwa satu-satunya hal yang ditegaskan oleh into_init adalah bahwa nilainya memenuhi _validity invariant_ dari T , yang jangan disamakan dengan T "diinisialisasi" dalam arti umum apa pun.

Sebagai contoh:

pub mod foo {
    pub struct AlwaysTrue(bool);
    impl AlwaysTrue { 
        pub fn new() -> Self { Self(true) }
        /// It is impossible to initialize `AlwaysTrue` to false
        /// and unsafe code can rely on `is_true` working properly:
        pub fn is_true(x: bool) -> bool { x == self.0 }
    }
}

pub unsafe fn improperly_initialized() -> foo::AlwaysTrue {
    let mut v: MaybeUninit<foo::AlwaysTrue> = MaybeUninit::uninitialized();
    // let v = v.into_init(); // UB: v is invalid
    *(v.as_mut_ptr() as *mut u8) = 3; // OK
    // let v = v.inti_init(); // UB v is invalid
    *(v.as_mut_ptr() as *mut bool) = false; // OK
    let v = v.into_init(); // OK: v is valid, even though AlwaysTrue is false
    v
}

Di sini nilai kembali dari improperly_initialized "diinisialisasi" dalam arti memenuhi _validity invariant_ dari T , tetapi tidak dalam arti memenuhi _safety invariant_ T , dan perbedaannya halus tetapi penting, karena dalam hal ini pembedaan inilah yang memerlukan improperly_initialized untuk dideklarasikan sebagai unsafe fn .

Saat sebagian besar pengguna berbicara tentang sesuatu yang "diinisialisasi", mereka biasanya tidak memiliki semantik "valid tetapi MaybeUnsafe" dari MaybeUninit::into_init .

Jika kita ingin terus terang tentang hal ini, kita dapat memiliki Invalid<T> dan Unsafe<T> , memiliki Invalid<T>::into_valid() -> Unsafe<T> , dan meminta pengguna untuk menulis uninit.into_valid().into_safe() . Kemudian di atas improperly_initialized akan menghasilkan Unsafe<T> , dan hanya setelah pengguna menetapkan nilai AlwaysTrue menjadi true benar, mereka dapat memperoleh T yang aman:

// note: this is now a safe fn
fn improperly_uninitialized() -> Unsafe<foo::AlwaysTrue>;
fn initialized() -> foo::AlwaysTrue {
    let mut v: Unsafe<foo::AlwaysTrue> = improperly_uninitialized();
    unsafe { v.as_mut_ptr() as *mut bool } = true;
    unsafe { v.into_safe() }
}

Perhatikan bahwa ini memungkinkan improperly_uninitialized menjadi aman fn , karena sekarang invarian bahwa AlwaysTrue tidak aman tidak dikodekan dalam "komentar" di sekitar fungsi, tetapi di jenis.

Saya tidak tahu apakah pendekatan yang menyakitkan dan menyiksa itu layak untuk dilakukan. MaybeUninit tujuannya adalah untuk berkompromi, untuk memungkinkan pengguna menangani memori yang tidak diinisialisasi dan tidak valid, tetapi tanpa menempatkan perbedaan ini di wajah pengguna. Saya pribadi berpikir bahwa kami tidak dapat mengharapkan pengguna mengetahui perbedaan ini kecuali kami menampilkannya secara eksplisit di wajah mereka, dan orang harus mengetahui perbedaan ini agar dapat menggunakan MaybeUninit dengan benar. Jika tidak, orang mungkin akan menulis fn improperly_uninitialized() -> AlwaysTrue sebagai fn , dan mengembalikan AlwaysTrue karena mereka "menginisialisasi" nya.

Satu hal yang juga dapat dilakukan dengan Invalid<T> dan Unsafe<T> adalah memiliki dua sifat, ValidityCheckeable dan UnsafeCheckeable , dengan dua metode, ValidityCheckeable::is_valid(Invalid<Self>) dan UnsafeCheckeable::is_safe(Unsafe<Self>) , dan memiliki metode Invalid::into_valid dan Unsafe::into_safe assert_validity! dan assert_safety! .

Alih-alih menulis invarian keamanan dalam komentar, Anda cukup menulis kode untuk cek tersebut.

Saya pikir ada baiknya untuk menyebutkan lagi bahwa satu-satunya hal yang ditegaskan into_init adalah bahwa nilai memenuhi invarian validitas T, yang tidak harus disamakan dengan T yang "diinisialisasi" dalam arti umum kata.

Ini benar. OTOH, saya merasa "diinisialisasi" adalah proxy yang masuk akal untuk ini dalam penjelasan pertama.

Jika tidak, orang mungkin akan menulis fn improperly_uninitialized () -> AlwaysTrue sebagai fn aman, dan hanya mengembalikan AlwaysTrue yang tidak aman karena yah, mereka "menginisialisasi" nya.

Saya merasa kita dapat membuat poin yang masuk akal bahwa ini tidak "diinisialisasi" dengan benar. Saya setuju bahwa kita memerlukan dokumentasi yang tepat tentang bagaimana kedua invarian ini berinteraksi di suatu tempat (dan saya tidak yakin di mana tempat terbaiknya), tetapi saya juga berpikir bahwa intuisi kebanyakan orang akan mengatakan bahwa improperly_uninitialized tidak oke berfungsi untuk mengekspor. "Mematahkan invarian orang lain" adalah konsep yang, menurut saya, muncul secara alami ketika Anda berpikir tentang "semua fungsi aman yang saya ekspor harus sedemikian rupa sehingga kode aman tidak dapat menggunakannya untuk menghancurkan kekacauan".

Satu hal yang juga bisa dilakukan dengan Invaliddan Tidak Amanmemiliki dua ciri, ValidityCheckeable dan UnsafeCheckeable, dengan dua metode, ValidityCheckeable :: is_valid (Invalid) dan UnsafeCheckeable :: is_safe (Tidak aman), dan memiliki metode Invalid :: into_valid dan Unsafe :: into_safe assert_validity! dan assert_safety! pada mereka.

Dalam sebagian besar kasus, invarian keamanan tidak dapat diperiksa. Bahkan invarian validitas kemungkinan tidak dapat diperiksa untuk referensi. (Nah ini tergantung sedikit pada bagaimana kita memfaktorkan banyak hal.)

@tokopedia

Bagaimana dengan nama baru, seperti pindah ..., konsumsi ..., atau ubah ... atau sesuatu? Saya pikir into_init (ialized) terlalu membingungkan; juga saya, itu menyiratkan nilai sedang diinisialisasi, ketika sebenarnya kami secara implisit menyatakan bahwa itu sudah diinisialisasi.

Bagaimana move_init menyampaikan "pernyataan" lebih dari into_init tidak?

assert_init(italized) telah disarankan sebelumnya.

Namun, perhatikan bahwa read atau read_initialized atau as_ptr().read juga tidak benar-benar mengatakan apa pun tentang menegaskan apa pun.

Jika kita ingin terus terang tentang hal ini, kita dapat memiliki Invalid<T> dan Unsafe<T> , memiliki Invalid<T>::into_valid() -> Unsafe<T> , dan mengharuskan pengguna untuk menulis uninit.into_valid().into_safe() . Kemudian di atas improperly_initialized akan menghasilkan Unsafe<T> , dan hanya setelah pengguna menetapkan nilai AlwaysTrue menjadi true benar, mereka dapat memperoleh T yang aman:

@gnlb. Hei, itu cukup bagus. Saya suka bahwa ini melemparkan perbedaan ke wajah pengguna dengan cara yang tidak dapat dihindari. Ini mungkin momen mengajar yang bagus. "validitas" dan "keamanan" yang akan membuat orang berpikir dua kali? uninit.into_valid().into_safe() tidak terlalu bertele-tele dibandingkan dengan uninit.assume_initialized() atau yang lainnya. Tentu saja untuk membuat perbedaan ini, pertama-tama kita harus menemukan kesepakatan seputar model. 😅 Saya merasa kita harus menyelidiki model ini lebih lanjut.

assert_init(italized) telah disarankan sebelumnya.

@RalfJung Kami juga memiliki assume_initialized karena @eternaleye (menurut saya). Lihat https://github.com/rust-lang/rust/issues/53491#issuecomment -440730699 dengan daftar pembenaran yang cukup menarik.

TBH Saya merasa memiliki dua jenis terlalu bertele-tele.

@RalfJung Bisakah kita menggali lebih dalam? mungkin dengan beberapa perbandingan contoh yang menurut Anda menunjukkan tingkat verbositas yang tinggi?

Hmm ... jika kita mempertimbangkan lebih banyak API verbose, maka

uninit.into_inner(uninit.assert_initialized());

bisa bekerja dengan baik secara semantik. Metode pertama mengembalikan token yang mencatat pernyataan Anda. Metode kedua mengembalikan tipe dalam, tetapi mengharuskan Anda untuk menegaskan itu valid.

Saya tidak sepenuhnya yakin ini sepadan dengan usaha ekstra, karena abstraksi mungkin hanya membuat orang lebih bingung dan dengan demikian cenderung membuat kesalahan.

Kami juga telah mengasumsikan_inisialisasi karena @eternaleye (saya pikir). Lihat # 53491 (komentar) dengan daftar pembenaran yang cukup menarik.

Adil. assume_initialized kedengarannya bagus untukku.

Atau mungkin assume_init ? Itu mungkin harus konsisten dengan konstruktor, MaybeUninit::uninit() vs MaybeUninit::uninitialized() - dan yang ini dijadwalkan untuk distabilkan dengan batch pertama, jadi kita harus segera melakukan panggilan itu.

@nicoburns Saya tidak melihat manfaat yang kami peroleh dari menambahkan tipuan melalui token di sini.

Bisakah kita menggali lebih dalam? mungkin dengan beberapa perbandingan contoh yang menurut Anda menunjukkan tingkat verbositas yang tinggi?

Jelas bahwa ini lebih bertele-tele daripada "hanya" MaybeUninit , bukan? Ada banyak beban mental tambahan (harus memahami dua jenis), ada yang membuka bungkus ganda, dan itu berarti saya harus memilih jenis yang akan digunakan. Jadi ada beberapa biaya tambahan di sini yang saya rasa perlu Anda benarkan.

Saya sebenarnya umumnya meragukan kegunaan Unsafe . Dari perspektif penyusun, ini akan menjadi NOP sepenuhnya; kompilator tidak pernah mengasumsikan bahwa data Anda memenuhi invarian keamanan. Dari perspektif implementasi perpustakaan, saya sangat meragukan bahwa keterbacaan kode akan meningkat jika, dalam implementasi Vec , kita mentransmutasikan sesuatu menjadi Unsafe<Vec<T>> setiap kali kita melanggar sementara keamanan invariant. Dan dari perspektif pengajaran, saya ragu siapa pun akan terkejut ketika mereka membuat Vec<T> yang valid tetapi tidak aman, berikan itu ke beberapa kode yang aman, dan kemudian semuanya meledak.
Bandingkan ini dengan MaybeUninit yang diperlukan dari perspektif kompilator, dan fakta bahwa Anda bahkan perlu berhati-hati tentang "buruk" bool dalam kode pribadi Anda mungkin mengejutkan beberapa orang. .

Mengingat biayanya yang signifikan, saya pikir Unsafe membutuhkan motivasi yang jauh lebih kuat. Saya tidak melihat bagaimana itu sebenarnya membantu mencegah bug atau meningkatkan keterbacaan kode.

Saya dapat melihat argumen untuk mengganti nama MaybeUninit menjadi MaybeInvalid . Namun, "tidak valid" sangat samar (tidak valid untuk apa ?), Saya telah melihat orang-orang bingung dengan perbedaan saya antara "valid" dan "aman" - orang mungkin menganggap bahwa "valid Vec " valid untuk segala jenis penggunaan. "Tidak dimulai" setidaknya pada dasarnya memicu asosiasi yang tepat bagi kebanyakan orang. Mungkin kita harus mengganti nama "validity invariant" menjadi "initialization invariant" atau lebih?

Selain itu, hanya adanya Unsafe<T> dapat menyesatkan (dengan menyiratkan secara salah bahwa semua nilai yang tidak dibungkus di dalamnya aman) kecuali kita mengadopsi konvensi luas yang kuat terhadap nilai tidak aman di luar pembungkus ini. Ini akan menjadi proyek besar, membutuhkan RFC lain dan konsensus komunitas yang lebih luas. Saya berharap ini menjadi agak kontroversial ( @RalfJung memberikan beberapa alasan bagus untuk menentangnya di atas), dan dengan argumen yang lebih lemah di sisinya daripada MaybeUninit karena tidak ada UB yang terlibat - ini pada dasarnya adalah pertanyaan gaya. Karena itu, saya skeptis apakah konvensi semacam itu akan pernah menjadi universal di komunitas Rust bahkan jika RFC diterima dan pustaka standar serta dokumen diperbarui.

Jadi IMO siapa pun yang ingin melihat bahwa konvensi terjadi memiliki ikan yang lebih besar untuk digoreng daripada bersepeda dengan API MaybeUninit , dan saya sarankan untuk tidak menunda stabilisasinya lebih jauh untuk menunggu penyelesaian proses itu. Jika kami menstabilkan konversi MaybeUninit<T> -> T , generasi Rust mendatang masih dapat menulis MaybeUninit<Unsafe<T>> untuk menunjukkan data yang pertama kali tidak diinisialisasi, dan kemudian mungkin masih tidak aman setelah diinisialisasi.

@Ralfian

Atau mungkin assume_init ? Itu mungkin harus konsisten dengan konstruktor, MaybeUninit::uninit() vs MaybeUninit::uninitialized() - dan _that_ one dijadwalkan untuk distabilkan dengan batch pertama, jadi kita harus segera melakukan panggilan itu.

Jika kita dapat memiliki konsistensi 3 arah dengan tipe, konstruktor, dan fungsi -> T itu akan menjadi lebih baik. Karena tipe tidak memiliki akhiran -ialized saya pikir ::uninit() dan .assume_init() mungkin cara yang tepat.

Jelas bahwa ini lebih bertele-tele daripada "hanya" MaybeUninit , bukan?

Tergantung ... Saya pikir foo.assume_init().assume_safe() (atau foo.init().safe() jika ada yang cenderung singkat) tidak terlalu lama. Kami juga dapat menawarkan kombinasi sebagai foo.assume_init_safe() jika perlu. Kombinasi tersebut masih memiliki keunggulan yaitu menjabarkan dua asumsi tersebut.

Ada banyak beban mental tambahan (harus memahami dua jenis), ada yang membuka bungkus ganda, dan itu berarti saya harus memilih jenis yang akan digunakan. Jadi ada beberapa biaya tambahan di sini yang saya rasa perlu Anda benarkan.

Mudah-mudahan kompleksitasnya muncul karena harus memahami konsep yang mendasari validitas dan keamanan. Setelah itu selesai, saya rasa tidak ada banyak kerumitan mental tambahan. Saya merasa konsep yang mendasarinya penting untuk disampaikan.

Saya sebenarnya secara umum meragukan kegunaan Unsafe . Dari perspektif penyusun, ini akan menjadi NOP sepenuhnya; kompilator tidak pernah mengasumsikan bahwa data Anda memenuhi invarian keamanan.

Tentu; Saya setuju bahwa dari kompiler POV itu tidak berguna. Kegunaan apa pun dari perbedaan tersebut adalah sebagai semacam antarmuka "jenis sesi".

Mengingat biayanya yang signifikan, saya pikir Unsafe membutuhkan motivasi yang jauh lebih kuat. Saya tidak melihat bagaimana itu sebenarnya membantu mencegah bug atau meningkatkan keterbacaan kode.

Aspek yang menarik perhatian saya adalah kemampuan mengajar. Saya pikir kesalahan pasti akan terjadi ketika orang berpikir bahwa .assume_init() berarti bahwa "OK; Saya telah memeriksa validitas invarian dan sekarang saya memiliki T ". Skema saat ini dari MaybeUninit<T> agak tidak membantu dengan cara ini. Namun saya belum menikah dengan Unsafe<T> dan Invalid<T> sebagai nama. Saya hanya berpikir bahwa pemisahan menjadi dua jenis, apapun namanya, dapat membantu secara pendidikan. Mungkin ada cara lain seperti memperkuat dokumentasi yang dapat mengimbangi hal ini dalam kerangka kerja saat ini?

Saya _dapat_ melihat argumen untuk mengganti nama MaybeUninit menjadi MaybeInvalid . Namun, "tidak valid" sangat kabur (tidak valid untuk _what_?), Saya telah melihat orang-orang bingung dengan perbedaan saya antara "valid" dan "aman" - orang mungkin berasumsi bahwa "valid Vec " valid untuk segala jenis penggunaan. "Tidak dimulai" setidaknya pada dasarnya memicu asosiasi yang tepat bagi kebanyakan orang. Mungkin kita harus mengganti nama "validity invariant" menjadi "initialization invariant" atau lebih?

Saya sangat setuju dengan "validitas" dan "keamanan" yang membingungkan karena cara suara yang "valid". Saya telah memilih "invarian mesin" sebagai pengganti "validitas" dan "jenis sistem invarian" untuk "keamanan".

@tokopedia

Jadi IMO siapa pun yang ingin melihat bahwa konvensi terjadi memiliki ikan yang lebih besar untuk digoreng daripada bersepeda dengan API MaybeUninit , dan saya menyarankan untuk tidak menunda stabilisasinya lebih jauh untuk menunggu resolusi dari proses itu. Jika kami menstabilkan konversi MaybeUninit<T> -> T , generasi Rust mendatang masih dapat menulis MaybeUninit<Unsafe<T>> untuk menunjukkan data yang pertama kali tidak diinisialisasi, dan kemudian mungkin masih tidak aman setelah diinisialisasi.

Poin bagus, terutama kembali. MaybeUninit<Unsafe<T>> ; Anda mungkin juga dapat menambahkan beberapa alias tipe untuk membuat nama tipe kurang bertele-tele.

Jika kita dapat memiliki konsistensi 3 arah dengan tipe, konstruktor, dan fungsi -> T itu akan menjadi lebih baik. Karena tipe tidak memiliki sufiks -ialized, saya pikir :: uninit () dan .assume_init () mungkin adalah cara yang tepat.

Sepakat. Saya agak sedih karena kehilangan awalan into , tetapi tidak melihat cara yang baik untuk mempertahankannya.

Jadi bagaimana dengan read / read_init ? Apakah kesamaan dengan ptr::read cukup untuk memicu "Anda sebaiknya memastikan ini benar-benar diinisialisasi"? Apakah read_init memiliki masalah yang mirip dengan into_init , di mana kedengarannya membuatnya diinisialisasi alih-alih menganggapnya sebagai asumsi? Mungkinkah assume_init menjadi seperti read sekarang?

Mudah-mudahan kompleksitasnya muncul karena harus memahami konsep yang mendasari validitas dan keamanan. Setelah itu selesai, saya rasa tidak ada banyak kerumitan mental tambahan. Saya merasa konsep yang mendasarinya penting untuk disampaikan.

Bisakah Anda memberikan contoh kode jika sesuatu di Vec dengan benar menggunakan ini untuk mencerminkan ketika invarian Vec dilanggar? Saya pikir itu akan sangat bertele-tele dan sepenuhnya mengaburkan apa yang sebenarnya terjadi.

Saya rasa menambahkan tipe seperti ini adalah cara yang salah untuk menyampaikan konsep yang mendasarinya.

Saya pikir kesalahan pasti akan terjadi ketika orang berpikir bahwa .assume_init () berarti "OK; saya telah memeriksa validitas invariant dan sekarang saya memiliki T yang baik".

Saya merasa sangat tidak mungkin ada orang yang seperti "Saya telah menginisialisasi Vec<i32> dengan menuliskannya penuh dengan 0xFF , sekarang sudah diinisialisasi, itu berarti saya dapat mendorongnya". Saya ingin melihat setidaknya indikasi, data solid yang lebih baik, bahwa ini sebenarnya adalah kesalahan yang dibuat orang.
Dalam pengalaman saya, orang memiliki intuisi yang cukup kuat bahwa ketika mereka membagikan data ke kode yang tidak dikenal, atau memanggil operasi perpustakaan pada beberapa data, maka invarian perpustakaan perlu ditegakkan.

Segalanya agak tenang di sini. Jadi bagaimana dengan rencana berikut:

  • Saya menyiapkan PR untuk menghentikan MaybeUninit::uninitialized dan mengganti namanya menjadi MaybeUninit::uninit .
  • Setelah itu mendarat (perlu memperbarui stdsimd, jadi ada beberapa waktu di sini jika orang berpikir ini bukan cara yang tepat), saya menyiapkan PR untuk menstabilkan MaybeUninit::{new, uninit, zeroed, as_ptr, as_mut_ptr} .

Ini membuat pertanyaan terbuka sekitar set / write , into_init[ialized] / assume_init[ialized] dan read[_init[italized]] . Saat ini, saya condong ke assume_init , write dan read , tetapi saya telah berubah pikiran tentang ini sebelumnya. Sayangnya saya tidak tahu bagaimana mengambil keputusan di sini.

  • Setelah itu mendarat

Apakah itu berarti bahwa akan ada periode ketika tidak ada jalan untuk membuat nilai yang tidak diinisialisasi tanpa (a) peringatan penghentian atau (b) menggunakan fitur yang tidak stabil? Itu bukanlah praktik yang berkelanjutan.

Saat menghentikan sesuatu yang tidak kami rencanakan untuk dihapus secara efektif, pengganti yang stabil harus tersedia setiap kali peringatan penghentian ditambahkan. Jika tidak, orang hanya akan menambahkan anotasi untuk mengabaikan peringatan tersebut dan melanjutkan hidup mereka.

Apakah itu berarti akan ada periode di mana tidak ada jalan untuk membuat nilai yang tidak diinisialisasi tanpa (a) peringatan penghentian atau (b) menggunakan fitur yang tidak stabil?

Saya bingung. Saya mengusulkan untuk menghentikan metode tidak stabil dan memperkenalkan metode tidak stabil lainnya sebagai gantinya.

Perhatikan bahwa saya berbicara tentang MaybeUninit::uninitialized , bukan mem::uninitialized .

Sayangnya saya tidak tahu bagaimana mengambil keputusan di sini.

@RalfJung Lakukan saja (dan

Lakukan saja (dan hubungi saya jika Anda suka) seperti yang Anda lakukan sebelumnya dengan penggantian nama PR lainnya dan jika ada yang keberatan, kami dapat mengatasinya di FCP. :)

Baiklah, saya akan menunggu sebentar karena ini tidak harus menjadi bagian dari stabilisasi awal.

menghentikan metode tidak stabil dan memperkenalkan metode tidak stabil lainnya sebagai gantinya

Ah, mengerti. Lanjutkan.

Baiklah, lakukan penggantian nama di https://github.com/rust-lang/rust/pull/59284 :

tidak diinisialisasi -> uninit
into_initialized -> mengasumsikan_init
read_initialized -> baca
set -> tulis

Saya suka nama yang baru diusulkan. Saya sedikit khawatir tentang read yang disalahgunakan, tetapi tampaknya itu jauh lebih kecil kemungkinannya daripada into_initialized disalahgunakan, terutama karena kaitannya dengan ptr::read . Secara keseluruhan, saya pikir penamaan baru benar-benar dapat diterima untuk stabilisasi.

Saya menyiapkan PR untuk menstabilkan MaybeUninit :: {new, uninit, zeroed, as_ptr, as_mut_ptr}.

Adakah kemungkinan ini bisa membuatnya menjadi 1,35-beta (dijadwalkan dalam ~ 2 minggu)?

Saya agak berkonflik tentang mendorong ini mengingat betapa masih belum terungkapnya https://github.com/rust-lang/rfcs/pull/2582 . : / Tanpa RFC itu, inisialisasi bertahap dari sebuah struct masih tidak mungkin dilakukan, tetapi orang akan tetap melakukannya.
OTOH, MaybeUninit telah menunggu cukup lama. Dan itu tidak seperti kode untuk inisialisasi bertahap yang orang tulis saat ini lebih baik daripada yang mereka tulis dengan MaybeUninit .

Meskipun demikian, https://github.com/rust-lang/rust/pull/59284 bahkan belum mendarat, jadi kami harus buru-buru memasukkannya ke 1,35. TBH Saya lebih suka menunggu satu siklus lagi sehingga orang mendapatkan setidaknya beberapa waktu untuk bermain dengan nama metode baru dan melihat bagaimana perasaan mereka.

Adakah kemungkinan bahwa fungsi konstruksi pada MaybeInit bisa menjadi const ?

init dan new adalah const . zeroed bukan, kita membutuhkan beberapa ekstensi untuk melakukan apa fungsi const sebelum bisa menjadi const .

Saya ingin memberikan beberapa umpan balik tentang MaybeUninit , perubahan kode sebenarnya dapat dilihat di sini https://github.com/Thomasdezeeuw/mio-st/pull/71. Secara keseluruhan, pengalaman saya (terbatas) dengan API itu positif.

Satu-satunya masalah kecil yang saya temui adalah bahwa mengembalikan &mut T dalam MaybeUninit::set harus menggunakan let _ = ... (https://github.com/Thomasdezeeuw/mio-st/pull/ 71 / files # diff-1b9651542d08c6eca04e6025b1c6fd53R116), yang agak canggung tetapi bukan masalah besar.

Saya juga harus menambahkan API yang saya inginkan saat bekerja dengan array unitialised, sering kali dikombinasikan dengan C.

  1. Metode untuk beralih dari &mut [MaybeUninit<T>] menjadi &mut [T] akan menyenangkan, pengguna harus memastikan bahwa semua nilai dalam irisan yang diinisialisasi dengan benar
  2. Fungsi atau makro penginisialisasi array publik, seperti uninitialized_array , juga akan menjadi tambahan yang sangat bagus.

Saya ingin memberikan masukan tentang MaybeUninit

Terima kasih banyak!

mengembalikan & mut T di MaybeUninit :: set menyebabkan harus menggunakan let _ = ...

Mengapa demikian? Anda hanya dapat "membuang" nilai yang dikembalikan, pada kenyataannya contoh di dokumen tidak let _ = ... . ( write / set belum memiliki contoh ... tapi sebenarnya hampir sama dengan read , mungkin seharusnya hanya menautkan.)

foo.write(bar); berfungsi dengan baik tanpa let .

bekerja dengan array yang disatukan

Ya, itu pasti bidang minat masa depan.

@Ralfian

mengembalikan & mut T di MaybeUninit :: set menyebabkan harus menggunakan let _ = ...

Mengapa demikian? Anda hanya dapat "membuang" nilai yang dikembalikan, pada kenyataannya contoh di dokumen tidak let _ = ... . ( write / set belum memiliki contoh ... tapi sebenarnya hampir sama dengan read , mungkin seharusnya hanya menautkan.)

Saya telah mengaktifkan peringatan untuk unused_results , jadi tanpa let _ = ... itu akan menghasilkan peringatan. Saya lupa itu bukan default.

Ah, saya tidak tahu tentang peringatan itu. Menarik.

Itu mungkin argumen untuk membuat write tidak mengembalikan referensi, dan menyediakan metode terpisah untuk itu jika ada lebih banyak permintaan.

Fungsi atau makro penginisialisasi array publik, seperti uninitialized_array , juga akan menjadi tambahan yang sangat bagus.

Ini hanya akan menjadi [MaybeUninit::uninit(); EVENTS_CAP] . Lihat https://github.com/rust-lang/rust/issues/49147.

Saya lupa itu bukan default.

Itu mungkin argumen untuk membuat write tidak mengembalikan referensi, dan menyediakan metode terpisah untuk itu jika ada lebih banyak permintaan.

Sepertinya ceruk? Jika ada lebih banyak permintaan di masa mendatang, kami dapat menambahkan metode yang tidak mengembalikan referensi.

Sepertinya ceruk?

Ya, ada banyak sekali metode yang menetapkan nilai dan kemudian mengembalikan referensi yang bisa berubah padanya.

@Centril Heh, saya rasa saya tidak melihat komentar Anda di sini ketika saya menulis ini di tempat lain: https://github.com/rust-lang/rust/issues/54542#issuecomment -478261027

Menghapus fungsi lama yang diganti namanya di https://github.com/rust-lang/rust/pull/59912.

Setelah itu, saya kira hal berikutnya yang harus dilakukan adalah mengusulkan stabilisasi ...: tada:

Saya sedikit berkonflik tentang mendorong ini mengingat betapa masih belum jelasnya karat-lang / rfcs # 2582 . : / Tanpa RFC itu, inisialisasi bertahap dari sebuah struct masih tidak mungkin dilakukan, tetapi orang akan tetap melakukannya.
OTOH, MaybeUninit telah menunggu cukup lama. Dan itu tidak seperti kode untuk inisialisasi bertahap yang orang tulis saat ini lebih baik daripada yang mereka tulis dengan MaybeUninit .

Setelah itu, saya kira hal berikutnya yang harus dilakukan adalah mengusulkan stabilisasi ... 🎉

@RalfJung Bagaimana status dokumentasi di sini? Jika kita dapat mengurangi "orang akan tetap melakukannya" dengan beberapa dokumen yang jelas yang akan membantu saya tidur lebih nyenyak ... :)

Membaca dokumen MaybeUninit , khususnya assume_init , tidak jelas di bagian "Keamanan" bahwa jika Anda memanggil mu.assume_init() dan kemudian mengembalikan hasil itu dalam brankas fn , maka Anda juga harus menjunjung invarian keamanan juga. Sebelum menstabilkan, alangkah baiknya untuk menyempurnakan dokumen tersebut dan memberikan cuplikan dengan pustaka yang menyediakan invarian keamanan yang juga harus dipertahankan saat menggunakan MaybeUninit .

Bagaimana status dokumentasi di sini? Jika kita dapat mengurangi "orang akan tetap melakukannya" dengan beberapa dokumen yang jelas yang akan membantu saya tidur lebih nyenyak ... :)

Saya mungkin akan menambahkan bagian tentang inisialisasi bertahap dari struct, mengatakan bahwa saat ini tidak didukung. Orang yang membaca ini akan seperti "WTF, benarkah?".

TBH Saya merasa ini agak membuat frustrasi. :( Saya pikir sangat mungkin bagi kami untuk memberikan beberapa saran untuk itu sekarang dan saya sedih karena kami tidak dapat melakukan itu.

tidak jelas di bagian "Keamanan" bahwa jika Anda memanggil mu.assume_init () dan kemudian mengembalikan hasil itu ke fn yang aman, Anda juga harus menjunjung invarian keamanan. Sebelum menstabilkan, alangkah baiknya untuk menyempurnakan dokumen tersebut dan memberikan cuplikan dengan pustaka yang disediakan invarian keamanan yang juga harus dipertahankan saat menggunakan MaybeUninit.

Anda pada dasarnya menyarankan untuk mengubahnya menjadi dokumen yang menjelaskan seluruh gagasan invarian tipe data dan bagaimana hasilnya di Rust. Saya pikir MaybeUninit adalah tempat yang salah untuk itu; yang akan membuatnya terdengar seperti kekhawatiran ini khusus untuk MaybeUninit padahal sebenarnya tidak. Hal-hal yang Anda tanyakan harus dijelaskan di tempat yang lebih tinggi seperti Nomicon. Saya berencana untuk memfokuskan dokumen MaybeUninit pada masalah inti jenis ini. Jangan ragu untuk mengembangkannya jika menurut Anda itu berguna. :)

Anda pada dasarnya menyarankan untuk mengubahnya menjadi dokumen yang menjelaskan seluruh gagasan invarian tipe data dan bagaimana hasilnya di Rust.

Ini agak kuat ... Saya hanya menyarankan "Oh, __ omong-omong__, ingat juga masalah keselamatan invarian" di beberapa tempat strategis dalam dokumentasi MaybeUninit<T> . Saya tidak menyarankan kami menambahkan novel. ;) Novel itu dapat berada di Nomicon tetapi kemungkinan besar kebanyakan orang yang menggunakan MaybeUninit<T> sebagian besar akan berinteraksi dengan dokumentasi perpustakaan standar.

Baiklah, saya mencoba memasukkan semua itu ke dalam PR stabilisasi: https://github.com/rust-lang/rust/pull/60445

Saya baru saja menemukan penggunaan mem::uninitialized dalam dokumentasi pustaka standar, tidak benar-benar tahu di mana lagi harus diperhatikan bahwa contoh terakhir core::ptr::drop_in_place perlu diperbarui (juga agak ironis bahwa itu menunjukkan bentuk lain dari UB yang hanya akan dikenakan sanksi oleh https://github.com/rust-lang/rfcs/pull/2582, jadi secara pribadi saya akan menghapusnya).

@HeroicKatora terima kasih! Saya memasukkan perbaikan untuk itu ke https://github.com/rust-lang/rust/pull/60445.

Kami tidak dapat benar-benar melakukan apa pun tentang bidang ref-to-unaligned saat ini, tidak yakin apakah menghapus dokumen adalah ide yang bagus.

Mungkin menambahkan sifat PartialUninit (atau PartialInit ) yang akan menginisialisasi sebagian data berdasarkan metadata.

Contoh: MODULEENTRY32W .
Bidang pertama ( dwSize ) harus diinisialisasi oleh ukuran struktur ( size_of::<MODULEENTRY32W>() ).

pub trait PartialUninit: Sized {
    fn uninit() -> MaybeUninit<Self>;
}

impl<T> PartialUninit for T {
    default fn uninit() -> MaybeUninit<Self> {
        MaybeUninit::uninit()
    }
}

impl PartialUninit for MODULEENTRY32W {
    unsafe fn uninit() -> MaybeUninit<MODULEENTRY32W> {
        let uninit = MaybeUninit { uninit: () };
        uninit.get_mut().dwSize = size_of::<MODULEENTRY32W>();
        uninit
    }
}

Bagaimana menurut Anda?

@kgv Saya khawatir saya tidak mengerti saran Anda. Mungkin beberapa konteks tambahan yang menjelaskan masalah apa yang Anda coba selesaikan dapat membantu? Dan mungkin contoh yang lebih lengkap dari solusi yang Anda sarankan?

@scottjmaddah diperbaiki . Apakah lebih jelas?

@kgv apa masalah yang sedang dipecahkan ini (sebagai lawan dari seseorang yang hanya menulis fungsi pembantu untuk ini)? Saya tidak mengerti mengapa libstd harus melakukan apa pun di sini.

Perhatikan bahwa inisialisasi parsial berbasis tugas dari struct hanya berfungsi untuk jenis yang tidak perlu dihapus. uninit.get_mut().foo = bar jika tidak akan turun foo , yang merupakan UB.

@RalfJung Masalah yang saya coba selesaikan - pekerjaan terpadu dengan struktur FFI, beberapa bidang di antaranya tidak bergantung pada self (hanya Self atau tidak bergantung pada apa pun (konstan)), misalnya - salah satu bidang berukuran Self .

@kgv Saya harus setuju dengan @RalfJung di sini bahwa kasus penggunaan seperti itu lebih baik ditangani oleh modul pembantu atau peti.

PR stabilisasi mendarat, tepat pada waktunya untuk beta. :) Sudah sekitar 8 bulan sejak saya mulai melihat situasi seputar serikat pekerja dan memori yang tidak diinisialisasi, dan akhirnya kami memiliki sesuatu yang (kemungkinan besar) akan dikirimkan dalam 6 minggu. Perjalanan yang luar biasa! Terima kasih banyak untuk semua orang yang membantu dengan itu. : D

Tentu saja, kami masih jauh dari selesai. Ada https://github.com/rust-lang/rfcs/pull/2582 yang harus diselesaikan. libstd masih memiliki beberapa kegunaan mem::uninitialized (kebanyakan dalam kode khusus platform) yang perlu porting. API stabil yang kita miliki sekarang sangat minimal: kita perlu memikirkan apa yang harus dilakukan dengan read dan write , dan kita harus membuat API yang membantu bekerja dengan array dan kotak MaybeUninit . Dan kami memiliki banyak penjelasan yang harus dilakukan untuk perlahan-lahan memindahkan seluruh ekosistem dari mem::uninitialized .

Tapi kita akan sampai di sana, dan langkah pertama ini mungkin yang paling penting. :)

dan kita harus membuat API yang membantu bekerja dengan array dan kotak MaybeUninit .

@RalfJung Untuk itu; mungkin sekarang saatnya untuk mulai mengerjakan https://github.com/rust-lang/rust/issues/49147? = P

Selain itu, kami mungkin harus membagi dan menutup masalah pelacakan ini demi bit yang lebih kecil untuk bit yang tersisa.

Untuk itu; mungkin sekarang saatnya untuk mulai mengerjakan # 49147? = P

Apakah Anda baru saja menjadi sukarelawan? ;) (Saya khawatir saya tidak akan punya waktu untuk itu.)

kami mungkin harus membagi dan menutup masalah pelacakan ini demi bit yang lebih kecil untuk bit yang tersisa.

Saya akan menyerahkan itu kepada ahli proses. Tapi saya cenderung setuju.

Apakah Anda baru saja menjadi sukarelawan? ;) (Saya khawatir saya tidak akan punya waktu untuk itu.)

Apa yang telah saya lakukan ... = D - Saya sudah memiliki proyek yang sedang saya kerjakan jadi mungkin akan memakan waktu. Mungkin orang lain tertarik? (jika demikian, masuklah ke masalah pelacakan)

Saya akan menyerahkan itu kepada ahli proses. Tapi saya cenderung setuju.

Itu akan saya ...;) Saya akan mencoba untuk membagi dan menutupnya segera-ish.

@RalfJung tentang pernyataan anda bahwa let x: bool = mem::uninitialized(); adalah UB, pertanyaannya kenapa primitif yang tidak valid dianggap demikian? Seperti yang saya pahami, Anda harus membaca nilai untuk mengamati bahwa tidak valid untuk memicu UB. Tetapi jika Anda tidak membacanya lalu apa?

Saya merasa bahwa bahkan menciptakan nilai adalah hal yang buruk, tetapi saya ingin tahu alasan mengapa karat tetap melarangnya? Tampaknya tidak ada salahnya jika Anda tidak mengamati keadaan yang tidak valid. Apakah itu hanya demi kesalahan awal atau mungkin sesuatu yang lain?

Apakah ada kasus nyata dalam compiler yang mengandalkan asumsi ini?

Misalnya, kami menganotasi fungsi seperti foo(x: bool) memberi tahu LLVM bahwa x adalah boolean yang valid. Itu membuat UB melewatkan bool yang bukan true atau false bahkan jika fungsi aslinya tidak melihat x . Ini berguna karena terkadang kompilator ingin memperkenalkan penggunaan variabel yang sebelumnya tidak digunakan (khususnya, ini terjadi ketika pernyataan keluar dari perulangan tanpa membuktikan bahwa perulangan dilakukan setidaknya sekali).

AFAIK kami juga menetapkan (atau ingin mengatur) beberapa penjelasan ini dalam suatu fungsi, tidak hanya pada batas fungsi. Dan kami mungkin menemukan lebih banyak tempat di masa mendatang di mana informasi semacam itu dapat berguna. Kita mungkin dapat menutupinya dengan definisi pintar "menggunakan variabel" (istilah yang Anda gunakan tanpa mendefinisikannya, dan memang tidak mudah untuk didefinisikan), tapi saya pikir ketika datang ke UB dalam kode yang tidak aman, itu adalah penting untuk memiliki aturan sederhana di mana kita bisa.

Jadi, kami ingin memastikan bahwa meskipun dalam kode yang tidak aman, tipe dalam kode memiliki arti. Itu hanya mungkin dengan memperlakukan memori yang tidak diinisialisasi dengan cara yang tepat dengan tipe khusus, alih-alih pendekatan ad-hoc "yolo" yang berbohong kepada kompiler tentang konten variabel ("Saya mengklaim ini adalah bool , tapi sungguh saya tidak akan menginisialisasinya ").

Misalnya, kami menganotasi fungsi seperti foo (x: bool) memberi tahu LLVM bahwa x adalah boolean yang valid. Itu membuatnya UB melewatkan bool yang tidak benar atau salah bahkan jika fungsi awalnya tidak melihat x. Ini berguna karena terkadang kompilator ingin memperkenalkan penggunaan variabel yang sebelumnya tidak digunakan (khususnya, ini terjadi ketika pernyataan keluar dari perulangan tanpa membuktikan bahwa perulangan dilakukan setidaknya sekali).

Ini dapat dianggap sebagai penggunaan. Saya bertanya tentang memasukkan nilai dan tidak pernah membaca / meneruskannya ke mana pun sebelum ditimpa dengan nilai yang valid.
Saya tidak melihat kasus penggunaan yang berguna untuk memasukkan nilai dengan cara yang bernuansa seperti itu, tetapi hanya bertanya-tanya.

Singkatnya, pertanyaan saya adalah apakah kode ini UB (menurut dokumen - itu), dan jika demikian, apa sebenarnya yang bisa rusak jika saya menulisnya?

let _: bool = unsafe { mem::unitialized };

Pertanyaan lain tentang subjek itu sendiri: kita tahu bahwa kita memiliki sintaks box yang memungkinkan Anda mengalokasikan memori secara langsung di heap, dan selalu berfungsi tidak seperti Box::new() yang terkadang menggunakan memori stackalloc. Jadi jika saya melakukan box MaybeUninit::new() dan kemudian mengisinya, bagaimana saya dapat mengubah Box<MaybeUninit<T>> menjadi Box<T> ? Haruskah saya menulis transmutasi atau apa? Mungkin saya hanya melewatkan poin ini dalam dokumentasi.

@Pzixel kita sebenarnya telah membahas interaksi antara Box dan MaybeUninit sudah ada di utas ini : smile:

@Centril memiliki sub-masalah untuk dibahas yang mungkin bagus ketika Anda memisahkan ini.

Ya, saya ingat diskusi itu, tapi saya tidak ingat api tertentu.

Singkatnya, saya ingin memiliki sesuatu seperti

fn into_inner<A,T>(value: A<MaybeUninit<T>>) -> A<T> { unsafe { std::mem::transmute() } }

Tetapi menurut saya tidak ada API seperti itu, dan tampaknya itu tidak dapat diimplementasikan tanpa dukungan compiler pada tahap evolusi bahasa ini.


Saya memikirkannya sedikit lagi dan tampaknya itu harus bekerja pada tingkat sarang apa pun. Jadi Vec<Result<Option<MaybeUninit<u8>>>> harus memiliki into_inner metode yang mengembalikan Vec<Result<Option<u8>>>

Saya berasumsi get_ref dan get_mut akan distabilkan pada saat yang sama (semua fitur menunjuk pada masalah ini). Apakah ada alasan untuk tidak melakukannya? Mereka bagus dan merupakan satu-satunya indikasi bahwa melakukan tindakan yang mereka lakukan diperbolehkan (yang seharusnya benar).

Ini dapat dianggap sebagai penggunaan.

Jadi let x: bool = mem::uninitialized() tidak menggunakan bool (meskipun itu ditugaskan ke x !), Tapi

fn id(x: bool) -> bool { x }
let x: bool = id(mem::uninitialized());

apakah menggunakannya? Bagaimana dengan

fn uninit() -> bool { mem::uninitialized() }
let x: bool = uninit();

Apakah pengembalian di sini berguna?

Ini dengan sangat cepat menjadi sangat halus. Jadi jawaban yang menurut saya harus kita berikan adalah bahwa setiap tugas (benar-benar setiap salinan, seperti dalam, setiap tugas setelah diturunkan ke MIR) adalah penggunaan, dan itu termasuk tugas dalam let x: bool = mem::uninitialized() .


Saya berasumsi get_ref dan get_mut akan distabilkan pada saat yang sama (semua fitur menunjuk pada masalah ini). Apakah ada alasan untuk tidak melakukannya? Mereka bagus dan merupakan satu-satunya indikasi bahwa melakukan tindakan yang mereka lakukan diperbolehkan (yang seharusnya benar).

Ini diblokir saat menyelesaikan https://github.com/rust-lang/unsafe-code-guidelines/issues/77 : apakah aman memiliki &mut bool yang mengarah ke memori yang tidak diinisialisasi? Saya pikir jawabannya harus "ya", tetapi orang tidak setuju.

Ini diblokir saat menyelesaikan rust-lang / unsafe-code-Guidelines # 77

Saya rasa pemblokiran tidak perlu dilakukan. Anda dapat menstabilkannya dan berkata "UBlah yang menggunakan ini jika memori tidak diinisialisasi" dan kemudian melunakkan persyaratannya jika kami menentukannya baik-baik saja. Ini adalah metode yang bagus untuk dimiliki setelah inisialisasi.

dan kemudian melunakkan persyaratan

Yang berarti bahwa jika saya membuat kode terhadap dokumentasi versi mendatang tetapi seseorang mengkompilasi kode saya menggunakan kompilator versi lama (kompatibel dengan API!), Sekarang ada UB?

@Tokopedia

Saya rasa pemblokiran tidak perlu dilakukan. Anda dapat menstabilkannya dan berkata "UBlah yang menggunakan ini jika memori tidak diinisialisasi" dan kemudian melunakkan persyaratannya jika kami menentukannya baik-baik saja. Ini adalah metode yang bagus untuk dimiliki setelah inisialisasi.

Bagi saya itu tampaknya sangat berbahaya. Mengapa tidak menulis &mut *foo.as_mut_ptr() ? Setelah semuanya diinisialisasi, mengapa itu tidak berhasil? IOW, sekarang saya bertanya-tanya tentang apa yang Anda katakan

satu-satunya indikasi bahwa melakukan tindakan yang mereka lakukan diperbolehkan

karena mengapa tidak ? Jika kami secara lengkap mencantumkan semua yang dapat Anda lakukan setelah Anda menginisialisasi nilainya, itu akan menjadi daftar yang panjang. ^^

@magetanbanget

Yang berarti bahwa jika saya membuat kode terhadap dokumentasi versi mendatang tetapi seseorang mengkompilasi kode saya menggunakan kompilator versi lama (kompatibel dengan API!), Sekarang ada UB?

Itu benar hari ini jika orang melakukan &mut *foo.as_mut_ptr() . Saya tidak melihat cara untuk menghindarinya.

Selain itu, hanya ada UB jika kita benar-benar harus mengubah apapun saat melakukan dokumentasi itu. Kalau tidak, kita dalam situasi yang aneh dimana akan ada UB jika kode yang sama itu dijalankan dengan compiler yang sama sebelum kita membuat jaminan, tapi sekarang kita jamin tidak ada UB lagi. UB adalah properti tidak hanya dari kompiler tetapi juga spesifikasi, dan spesifikasi dapat berubah secara retroaktif. ;)

Benar, saya berasumsi prosesnya

  • menstabilkannya dengan persyaratan yang ketat tetapi tidak berarti implementasi sekarang
  • lanjutkan bekerja pada model memori dan apa yang Anda miliki
  • setelah model selesai

    • kalau perlu UB, kerennya biarkan saja dokumentasinya, tambah optimasi kalau berguna

    • jika tidak perlu UB, keren, lepaskan dari dokumen dan batalkan sehari

@Ralfian

Apakah pengembalian di sini berguna?

Ya, mengembalikan nilai atau meneruskannya ke mana pun adalah penggunaan.

Ini dengan sangat cepat menjadi sangat halus. Jadi jawaban yang menurut saya harus kita berikan adalah bahwa setiap tugas (benar-benar setiap salinan, seperti dalam, setiap tugas setelah diturunkan ke MIR) adalah sebuah kegunaan, dan itu termasuk tugas di let x: bool = mem :: uninitialized ().

Tampak valid.

Bagaimanapun, itu tentang arbitrart MaybeUninit bersarang? Bisakah itu diubah dengan aman tanpa mengharuskan pengguna menulis transmutasi untuk setiap jenis pembungkus?

@Pzixel Saya tidak yakin apakah saya memahami pertanyaan Anda, tetapi saya pikir pertanyaan itu sedang dibahas di https://github.com/rust-lang/rust/issues/61011.

Saya melihat bahwa metode MaybeUninit::write() yang masih tidak stabil bukanlah unsafe meskipun dapat melewati pemanggilan drop pada T , yang saya anggap tidak aman. Apakah ada preseden untuk ini dianggap aman?

https://doc.rust-lang.org/nomicon/leaking.html#leaking
https://doc.rust-lang.org/nightly/std/mem/fn.forget.html

forget tidak ditandai sebagai unsafe , karena jaminan keamanan Rust tidak termasuk jaminan bahwa penghancur akan selalu berjalan.

Bisakah kita menambahkan metode MaybeUninit<T> -> NonNull<T> ke MaybeUninit ? AFAICT pointer yang dikembalikan oleh MaybeUninit::as_mut_ptr() -> *mut T tidak pernah null. Itu akan mengurangi churn karena harus berinteraksi dengan API yang menggunakan NonNull<T> , dari:

let mut x = MaybeUninit<T>::uninit();
foo(unsafe { NonNull::new_unchecked(x.as_mut_ptr() });

untuk:

let mut x = MaybeUninit<T>::uninit();
foo(x.ptr());

pointer yang dikembalikan oleh MaybeUninit :: as_mut_ptr () -> * mut T tidak pernah null.

Ini benar.

Secara umum (dan saya pikir saya telah melihat @Gankro mengatakan ini), NonNull bekerja cukup baik "saat istirahat" tetapi ketika benar-benar menggunakan pointer seseorang ingin mendapatkan pointer mentah ASAP. Itu jauh lebih mudah dibaca.

Namun, menambahkan metode yang mengembalikan NonNull tampaknya baik-baik saja. Tapi apa yang harus disebut? Apakah ada yang diutamakan?

Ada preseden dengan https://github.com/rust-lang/rust/issues/47336 tetapi namanya tidak bagus dan saya tidak yakin kami akan menstabilkan metode ini.

Apakah kawah yang disebutkan di https://github.com/rust-lang/rust/pull/60445#issuecomment -488818677 telah terjadi?

Gagasan tentang 3 bulan waktu tersedia yang disebutkan @centril tidak terwujud bagi orang yang ingin bebas peringatan di semua versi beta, stabil, dan malam. 1.36.0 telah dirilis kurang dari seminggu yang lalu dan nightly sudah mengeluarkan peringatan.

Mungkinkah penghentian dapat ditunda ke 1.40.0?

Peringatan penghentian tidak selalu diisolasi ke peti yang bertanggung jawab atasnya. Misalnya, saat peti mengekspos makro yang menggunakan std::mem::uninitialized internal, penggunaan oleh peti pihak ketiga masih memanggil peringatan penghentian. Saya memperhatikan ini hari ini ketika saya mengkompilasi salah satu proyek saya dengan kompiler nightly. Meskipun kode tidak berisi satu pun penyebutan uninitialized , saya mendapat peringatan penghentian karena memanggil makro implement_vertex glium.

Menjalankan cargo +nightly test pada glium master memberi saya lebih dari 1.400 baris output, sebagian besar terdiri dari peringatan deprecation dari fungsi uninitialized (Saya menghitung peringatan 200 kali, tetapi kemungkinan dibatasi sebagai angka rg "uninitialized" | wc -l keluaran adalah 561).

Apa kekhawatiran tersisa yang menghalangi stabilisasi metode lainnya? Melakukan semuanya melalui *foo.as_mut_ptr() menjadi sangat membosankan, dan terkadang (untuk write ) melibatkan lebih banyak unsafe blok daripada yang diperlukan.

@SimonSapin Untuk meniru write , Anda dapat mengganti seluruh MaybeUninit dengan tidak aman menggunakan *val = MaybeUninit::new(new_val) dimana val: &mut MaybeUninit<T> dan new_val: T atau Anda dapat menggunakan std::mem::replace jika Anda menginginkan nilai lama.

@ est31 ini adalah poin yang bagus. Saya akan baik-baik saja mendorong kembali penghinaan dengan satu atau dua rilis.

Ada keberatan?

Kami telah mengatakan di entri blog rilis 1.36.0:

Sebagai MaybeUninitadalah alternatif yang lebih aman, dimulai dengan Rust 1.38, fungsi mem :: uninitialized tidak akan digunakan lagi.

Karena itu, saya pikir kita harus menghindari flip-floppery yang satu ini karena itu tidak mengirim pesan yang baik dan membingungkan. Selain itu, tanggal penghentian juga harus diketahui secara luas karena telah disebutkan dalam posting blog.

Mungkin sudah terlambat untuk kembali ke penghentian uninitialized . Tapi mungkin kita bisa memutuskan kebijakan untuk hanya mengeluarkan peringatan penghentian di Nightly setelah penggantinya berada di saluran Stabil selama beberapa waktu?

Misalnya, Firefox dikompromikan karena membutuhkan versi Rust baru dua minggu setelah dirilis .

Kami telah mengatakan di entri blog rilis 1.36.0:

Saya tidak setuju bahwa menyebutkan tanggal dalam posting blog adalah hal yang sangat sulit. Ada dalam repo dan kami dapat mengirimkan hasil edit.

Karena itu, saya pikir kita harus menghindari flip-floppery yang satu ini karena itu tidak mengirim pesan yang baik dan membingungkan.

"flip-floppery" adalah hal yang buruk, tetapi mengubah pikiran kita berdasarkan data dan umpan balik bukanlah itu.

Saya tidak terlalu peduli dengan keputusan yang sebenarnya, tetapi saya tidak percaya bahwa orang akan bingung dengan proposal tersebut. Mereka yang telah melihat postingan blog atau peringatan penghentian dapat pindah ke hal baru. Orang yang tidak hanya tidak akan peduli untuk beberapa rilis lainnya.

"flip-floppery" adalah hal yang buruk, tetapi mengubah pikiran kita berdasarkan data dan umpan balik bukanlah itu.

Sepenuhnya setuju. Saya tidak melihat pesan buruk dikirim dengan mengatakan "hei, jadwal penghentian kami agak terlalu agresif, kami mengembalikannya dengan rilis". Justru sebaliknya.
Faktanya IIRC I menyebutkan selama pendaratan PR stabilisasi bahwa presedennya adalah menghentikan 3 rilis di masa depan dan bukan 2, tetapi untuk beberapa alasan kami menggunakan 2. Tiga rilis berarti 1 rilis keseluruhan antara stable-gets-release-with-the -pengumuman-penghentian dan penghentian-pada-malam, sepertinya waktu yang tepat bagi orang-orang yang melacak setiap malam. 6 minggu itu eon, kan? ;)

Jadi saya berencana untuk mengirimkan PR besok yang mengubah versi deprecation menjadi 1.39.0. Saya juga bisa mengirimkan PR untuk memperbarui posting blog itu jika orang menganggapnya penting.

Jadi saya berencana untuk mengirimkan PR besok yang mengubah versi deprecation menjadi 1.39.0. Saya juga bisa mengirimkan PR untuk memperbarui posting blog itu jika orang menganggapnya penting.

Saya akan setuju dengan 1.39 tapi tidak lebih dari itu. Anda juga perlu memperbarui catatan rilis selain postingan blog.

PR yang dikirimkan untuk jadwal penghentian yang diubah: https://github.com/rust-lang/rust/pull/62599.

@Septianjoko_

Apa kekhawatiran tersisa yang menghalangi stabilisasi metode lainnya? Melakukan semuanya melalui * foo.as_mut_ptr () menjadi sangat membosankan, dan terkadang (untuk menulis) melibatkan lebih banyak blok yang tidak aman daripada yang diperlukan.

Untuk as_ref / as_mut , sejujurnya saya ingin menunggu sampai kami tahu apakah referensi harus menunjuk ke data yang diinisialisasi. Jika tidak, dokumentasi untuk metode tersebut hanyalah permulaan.

Untuk read / write , saya akan menstabilkannya dengan baik jika semua orang setuju bahwa nama dan tanda tangan itu masuk akal. Saya pikir ini harus dikoordinasikan dengan ManuallyDrop::take/read , dan mungkin juga harus ada ManuallyDrop::write ?

Sejujurnya saya ingin menunggu sampai kami tahu apakah referensi harus mengarah ke data yang diinisialisasi.

Apa yang diperlukan WG Pedoman Kode Tidak Aman dan Tim Bahasa untuk mengambil keputusan tentang subjek ini? Apakah Anda berharap itu akan terjadi dalam beberapa minggu, beberapa bulan, atau beberapa tahun?

Sementara itu, as_mut tidak stabil tidak menghentikan pengguna untuk menulis &mut *manually_drop.as_mut_ptr() saat mereka perlu menyelesaikan sesuatu.

Apa yang diperlukan WG Pedoman Kode Tidak Aman dan Tim Bahasa untuk mengambil keputusan tentang subjek ini? Apakah Anda berharap itu akan terjadi dalam beberapa minggu, beberapa bulan, atau beberapa tahun?

Berbulan-bulan, mungkin bertahun-tahun.

Sementara itu, as_mut menjadi tidak stabil tidak menghentikan pengguna untuk menulis & mut * manual_drop.as_mut_ptr () saat mereka perlu menyelesaikan sesuatu.

Ya saya tahu. Harapannya adalah untuk mendorong orang agar menunda bagian &mut sebanyak mungkin, dan bekerja dengan petunjuk mentah. Tentu saja tanpa https://github.com/rust-lang/rfcs/pull/2582 itu seringkali sulit.

Dokumentasi di MaybeUninit tampaknya menjadi tempat utama untuk setidaknya membahas bahwa ini adalah ambiguitas dalam semantik bahasa dan bahwa pengguna harus secara konservatif menganggap itu tidak baik.

Benar, itu akan menjadi pilihan lain.

Bahkan dengan asumsi konservatif, as_mut valid setelah nilai diinisialisasi sepenuhnya.

Salah satu cara untuk menjadi konservatif dengan array menggunakan MaybeUninit<[MaybeUninit<Foo>; N]> . Wrappers luar memungkinkan membuat array dengan satu panggilan uninit() . (Saya pikir [expr; N] literal membutuhkan Copy ?) Pembungkus bagian dalam membuatnya aman bahkan dalam asumsi konservatif untuk menggunakan kenyamanan slice::IterMut untuk melintasi array, dan lalu inisialisasi nilai Foo satu per satu.

@SimonSapin melihat uninitialized_array! tidak stabil

@RalfJung mungkin uninit_array! akan menjadi nama yang lebih baik.

@Stargateur Tentu saja, ini pasti tidak akan distabilkan dengan namanya saat ini. Mudah-mudahan tidak akan pernah stabil jika https://github.com/rust-lang/rust/issues/49147 terjadi Segera (TM).

@RalfJung Ugh, itu salah saya, saya memblokir PR tanpa alasan yang bagus: https://github.com/rust-lang/rust/pull/61749#issuecomment -512867703

@eddyb ini berfungsi untuk libcore, yay! Tetapi entah bagaimana ketika saya mencoba menggunakan fitur di liballoc, itu tidak dapat dikompilasi meskipun saya menyetel benderanya. Lihat https://github.com/rust-lang/rust/commit/4c2c7e0cc9b2b589fe2bab44173acc2170b20c09.

Building stage1 std artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
   Compiling alloc v0.0.0 (/home/r/src/rust/rustc.2/src/liballoc)
error[E0277]: the trait bound `core::mem::MaybeUninit<K>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<K>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:109:19
    |
109 |               keys: uninit_array![_; CAPACITY],
    |                     -------------------------- in this macro invocation
    |
    = help: consider adding a `where core::mem::MaybeUninit<K>: core::marker::Copy` bound
    = note: the `Copy` trait is required because the repeated element will be copied

error[E0277]: the trait bound `core::mem::MaybeUninit<V>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<V>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:110:19
    |
110 |               vals: uninit_array![_; CAPACITY],
    |                     -------------------------- in this macro invocation
    |
    = help: consider adding a `where core::mem::MaybeUninit<V>: core::marker::Copy` bound
    = note: the `Copy` trait is required because the repeated element will be copied

error[E0277]: the trait bound `core::mem::MaybeUninit<collections::btree::node::BoxedNode<K, V>>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<collections::btree::node::BoxedNode<K, V>>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:162:20
    |
162 |               edges: uninit_array![_; 2*B],
    |                      --------------------- in this macro invocation
    |
    = help: the following implementations were found:
              <core::mem::MaybeUninit<T> as core::marker::Copy>
    = note: the `Copy` trait is required because the repeated element will be copied

error: aborting due to 3 previous errors

Misteri dipecahkan: penggunaan ekspresi berulang di libcore benar-benar ada untuk jenis yang copy.

Dan alasan mengapa tidak berfungsi di liballoc adalah karena MaybeUninit::uninit tidak dapat dipromosikan.

@RalfJung Mungkin membuka PR menghapus penggunaan makro di mana itu sama sekali tidak perlu?

@eddyb Saya membuat bagian itu dari https://github.com/rust-lang/rust/pull/62799.

Mengenai maybe_uninit_ref

Untuk as_ref / as_mut, sejujurnya saya ingin menunggu sampai kami tahu apakah referensi harus menunjuk ke data yang diinisialisasi. Jika tidak, dokumentasi untuk metode tersebut hanyalah permulaan.

get_ref / get_mut yang tidak stabil pasti disarankan karena itu; namun, ada kasus di mana get_ref / get_mut dapat digunakan ketika MaybeUninit telah digunakan: untuk mendapatkan pegangan yang aman ke data (yang sekarang sudah diinisialisasi) sambil menghindari memcpy ( bukan assume_init , yang dapat memicu memcpy ).

  • ini mungkin tampak seperti situasi yang sangat spesifik, tetapi alasan utama orang (ingin) menggunakan data yang tidak diinisialisasi justru untuk jenis tabungan murah ini.

Karena ini, saya membayangkan assume_init_by_ref / assume_init_by_mut bisa menyenangkan untuk dimiliki (karena into_inner telah disebut assume_init , saya tampaknya masuk akal bahwa ref / ref mut getter juga mendapatkan nama khusus untuk mencerminkan ini).

Ada dua / tiga opsi untuk ini, terkait dengan Drop interaksi:

  1. API yang sama persis seperti get_ref dan get_mut , yang dapat menyebabkan kebocoran memori bila ada lem jatuh;

    • (Varian): API yang sama dengan get_ref / get_mut , tetapi dengan Copy terikat;
  2. API gaya penutupan, untuk menjamin penurunan:

impl<T> MaybeUninit<T> {
    /// # Safety
    ///
    ///   - the contents must have been initialised
    unsafe
    fn assume_init_with_mut<R, F> (mut self: MaybeUninit<T>, f: F) -> R
    where
        F : FnOnce(&mut T) -> R,
    {
        if mem::needs_drop::<T>().not() {
            return f(unsafe { self.get_mut() });
        }
        let mut this = ::scopeguard::guard(self, |mut this| {
            ptr::drop_in_place(this.as_mut_ptr());
        });
        f(unsafe { MaybeUninit::<T>::get_mut(&mut *this) })
    }
}

(Di mana logika scopeguard dapat dengan mudah diterapkan ulang, jadi tidak perlu bergantung padanya)


Ini dapat distabilkan lebih cepat dari get_ref / get_mut , mengingat persyaratan eksplisit assume_init .

Kekurangan

Jika varian opsi .1 dipilih, dan get_ref / get_mut dapat digunakan tanpa situasi assume_init , maka API ini akan menjadi hampir sepenuhnya lebih rendah (Saya katakan hampir karena dengan API yang diusulkan, membaca dari referensi akan baik-baik saja, yang tidak akan pernah terjadi dalam kasus get_ref dan get_mut )

Mirip dengan apa yang @danielhenrymantilla tulis tentang get_{ref,mut} , saya mulai berpikir bahwa read mungkin harus diganti namanya menjadi read_init atau read_assume_init atau lebih, sesuatu yang menunjukkan bahwa ini hanya dapat dilakukan setelah inisialisasi selesai.

@RalfJung Saya punya pertanyaan tentang ini:

fn foo<T>() -> T {
    let newt = unsafe { MaybeUninit::<T>::zeroed().assume_init() };
    newt
}

Misalnya, kami menyebut foo<NonZeroU32> . Apakah ini memicu UB ketika kita mendeklarasikan fungsi foo (karena harus valid untuk semua T s atau ketika kita instantinasinya dengan tipe yang memicu UB? Maaf jika tempat yang salah untuk berikan pertanyaan.

Kode @Pzixel hanya dapat menyebabkan UB saat dijalankan.

Jadi, foo::<i32>() baik-baik saja. Tapi foo::<NonZeroU32>() adalah UB.

Properti yang valid untuk semua cara yang mungkin untuk memanggil disebut "kesehatan", lihat juga referensi . Kontrak umum di Rust adalah bahwa permukaan API yang aman dari perpustakaan harus baik. Hal ini agar pengguna perpustakaan tidak perlu khawatir dengan UB. Seluruh kisah keamanan Rust bertumpu pada pustaka dengan API suara.

@Ralf terima kasih.

Jadi jika saya mengerti dengan benar fungsi ini tidak sehat (dan karenanya, tidak valid), tetapi jika kita menandainya sebagai unsafe maka badan ini menjadi valid dan sehat

@Pzixel jika Anda menandainya sebagai tidak aman, kesehatan bukanlah konsep yang bahkan berlaku lagi. "Apakah ini terdengar" hanya masuk akal sebagai pertanyaan untuk kode aman.

Ya, Anda harus menandai fungsi unsafe karena beberapa input dapat memicu UB. Namun meskipun demikian, masukan tersebut tetap memicu UB, sehingga fungsinya tetap tidak boleh disebut demikian. Tidak pernah boleh memicu UB, bahkan dalam kode yang tidak aman.

Ya, tentu saja, saya mengerti itu. Saya hanya ingin menyimpulkan bahwa fungsi parsial harus ditandai sebagai unsafe . Masuk akal bagi saya, tetapi saya tidak memikirkannya sebelum Anda menjawab.

Karena diskusi tentang masalah pelacakan ini sudah berlangsung lama, dapatkah kita memecahnya menjadi beberapa masalah pelacakan lain untuk setiap fitur MaybeUninit yang masih tidak stabil?

  • maybe_uninit_extra
  • maybe_uninit_ref
  • maybe_uninit_slice

Sepertinya masuk akal. Ada juga https://github.com/rust-lang/rust/issues/63291.

Menutup ini untuk mendukung terbitan meta yang melacak MaybeUninit<T> lebih umum: # 63566

Apakah halaman ini membantu?
0 / 5 - 0 peringkat