チェックされない初期化されていないメモリ
この規則の興味深い例外に、配列があります。安全な Rust は、配列を部分的に初期化することを
認めません。配列を初期化するとき、 let x = [val; N]
を用いて
全ての値を初期化するか、 let x = [val1, val2, val3]
を用いて、
それぞれの要素の値を個別に指定するかのどちらかが出来ます。残念ながら、
特によりインクリメンタルなやり方や、動的な方法で配列を初期化する必要がある場合、
これは非常に融通が利きません。
アンセーフな Rust では、この問題に対処するパワフルなツールが用意されています。 mem::uninitialized
です。
この関数は本当に何もせず、値を返すふりをします。これを利用することで、 Rust に
変数が初期化されたと見なさせることができ、状況に応じた、インクリメンタルな初期化を
行ないトリッキーなことが出来ます。
残念ながら、これによってあらゆる種類の問題が浮かび上がります。
変数が初期化されていると Rust が思っているか、思っていないかによって、
代入は異なる意味を持ちます。もし初期化していないと思っている場合、 Rust は、
セマンティクス的には単にビットを初期化していないメモリにコピーし、他には
何もしません。しかし、もし値が初期化していると思っている場合、 Rust は
古い値を Drop
しようとします! Rust に、値が初期化されていると信じ込ませるよう
トリックをしたので、もはや安全には普通の代入は使えません。
生のシステムアロケータを使用している場合も問題となります。このアロケータは、 初期化されていないメモリへのポインタを返すからです。
これに対処するには、 ptr
モジュールを使用しなければなりません。
特にこのモジュールは、古い値をドロップせずに、メモリ上の場所に値を代入することが
可能となる 3 つの関数を提供しています: write
、copy
、copy_nonoverlapping
です。
ptr::write(ptr, val)
はval
を受け取り、ptr
が指し示すアドレスに受け取った値を 移します。ptr::copy(src, dest, count)
は、 T 型のcount
が占有するビット数だけ、 src から dest に コピーします。 (これは memmove と同じです -- 引数の順序が逆転していることに注意してください!)ptr::copy_nonoverlapping(src, dest, count)
はcopy
と同じことをしますが、 2 つのメモリ領域が 重なっていないと見なしているため、若干高速です。 (これは memcpy と同じです -- 引数の 順序が逆転していることに注意してください!)
言うまでもないのですが、もしこれらの関数が誤用されると、甚大な被害を引き起こしたり、 未定義動作を引き起こすでしょう。これらの関数自体が必要とする唯一のものは、 読み書きしたい場所がアロケートされているということです。しかし、 任意のビットを任意のメモリの場所に書き込むことでものを壊すようなやり方は数え切れません!
これらを全部一緒にすると、以下のようなコードとなります。
#![allow(unused)] fn main() { use std::mem; use std::ptr; // 配列の大きさはハードコードされていますが,簡単に変えられます。 // これは、配列を初期化するのに [a, b, c] という構文を使えないことを意味しますがね! const SIZE: usize = 10; let mut x: [Box<u32>; SIZE]; unsafe { // Rust に x が完全に初期化されたと思わせます x = mem::uninitialized(); for i in 0..SIZE { // 非常に注意深く、それぞれのインデックスを読み込まずに上書きします // 注意: 例外安全性は考慮されていません。 Box はパニックできません ptr::write(&mut x[i], Box::new(i as u32)); } } println!("{:?}", x); }
Drop
を実装していない型や、 Drop
を実装する型を含まない型との、ptr::write
スタイルの
いたずらを心配しなくてよいということは注目に値します。なぜなら Rust は、これらをドロップしようと
しないと知っているからです。同じように、もし部分的に初期化されている構造体のフィールドに Drop
を
実装しているものが存在しない場合、このフィールド群に直接代入できるようにするべきです。
しかし、初期化されていないメモリを扱うとき、生成した値を Rust が、以前に 完全に初期化されたものと見なしてドロップしようとしてしまわないか、常に警戒する必要があります。 値がデストラクタを持つ場合、変数のスコープを通り抜ける全てのコントロールパスは、終了時までに その値を初期化する必要があります。これはコードパニックを含みます
まあ、初期化されていないメモリを扱うことに関してはこんなものです。 基本的にどのような場所でも、初期化されていないメモリが渡されることは予期していません。 ですからもしそのようなメモリを分配する場合、確実に本当に注意深く行なってください。