Box
やVec
は与えられた型に応じてヒープ領域を確保してくれますが、確保するメモリサイズが0であるときの詳しい挙動を調べてみました。
Box<T>
のT
がゼロサイズの場合
Rustでは、次のような型のサイズは0になります。
()
[u8; 0]
struct Foo;
enum Bar;
これらの型の値をBox
で生成したらどうなるのでしょうか。例として、ユニット型()
をBox
で確保した場合、そのポインタはどこを指しているのか調べてみます。
let b = Box::new(());
let p = &*b as *const ();
println!("{:p}", p);
結果は次のようになります。
0x1
Box
を使えば、ヒープ領域を確保してくれるはずですが、この場合は明らかにポインタがヒープ領域を指していません。どうやら、ゼロサイズの型については、Box
はダングリングポインタになるようです。といっても、ゼロサイズの場合はポインタの先を参照することは無いので、未定義動作にはなりません。
T
がゼロサイズの時、Box<T>
がダングリングポインタにになることで、ジェネリックなコードが書きやすくなっています。なぜなら、プログラマがT
のサイズを気にすることなく、Box
側がヒープ確保のオーバーヘッドが最小になるように面倒をみてくれるためです。
長さ0のVec
Vec::new()
やvec![]
により、長さ(要素の数)が0のVecを作ることができます。このとき作ったVec
はどこを指しているのか調べてみます。
let v: Vec<i32> = vec![];
let p = v.as_ptr();
println!("{:p}", p);
let v: Vec<i32> = Vec::new();
let p = v.as_ptr();
println!("{:p}", p);
実行結果は次のようになります。
0x4
0x4
Vec
についてもダングリングポインタになり、ヒープ領域の確保は行われていません。これでヒープ領域割り当てのコストを心配すること無しに長さ0のVec
を作ることができます 1。長さが0でもヒープ領域を確保したい場合は、Vec::with_capacity()
を使いましょう。また、T
そのものがゼロサイズであれば、Vec<T>
はいかなる場合でもヒープ領域を確保しません。
ダングリングポインタについて
上の例に示したBox
やVec
が、なぜnullではなく0x1や0x4というアドレスを指すダングリングポインタになっているのでしょうか。これは、Boxは絶対に0(null)にならないことを保証しているためです。これにより、Option<Box<T>>
がBox<T>
と同じサイズとなるわけです。Vecの場合は、内部では絶対にnullにならないポインタ型Unique<T>
2 を使っているため、こちらもヒープ領域を確保しない場合はダングリングポインタが格納されます。
ちなみに、Rustには公式にダングリングポインタを作る方法が用意されています。NonNull<T>
という型は、絶対にnullにならないような制約をつけたポインタ型ですが、これにはdangling()
というメソッドが存在し、次のように実装されています(一部省略)。
impl<T: Sized> NonNull<T> {
pub fn dangling() -> Self {
unsafe {
let ptr = mem::align_of::<T>() as *mut T;
NonNull::new_unchecked(ptr)
}
}
}
この実装では、生成されたダングリングポインタのアドレスはその型のアラインメントのサイズと同じです。空のVec<i32>
の指している先が0x4なのも同様に、i32
のアライメントが4のためだったことが分かります。たとえダングリングポインタでも、ポインタのアラインメントの規則は守られているわけですね。
ダングリングポインタの作り方や、Box
やVec
の内部表現は、普通にRustを書く上で気にする必要はありません。しかし、もしヒープ領域の確保・解放が関わるunsafeなコードを書く場合は、これらの知識が必要になるでしょう。