サイズが 0 の型を扱う
時間です。サイズが 0 の型という怪物と戦いましょう。安全な Rust では絶対に これを気にする必要はないのですが、 Vec は非常に生ポインタや生アロケーションを 多用します。これらはまさにサイズが 0 の型に注意しなくてはいけないところです。 以下の 2 つを気にしなければなりません。
- 生アロケータ API は、もしアロケーションのサイズとして 0 を渡すと、 未定義動作を引き起こします。
- 生ポインタのオフセットは、サイズが 0 の型に対しては no-op となります。 これによって C スタイルのポインタによるイテレータが壊れます。
ありがたいことに、ポインタのイテレータと、 RawValIter と RawVec に対する アロケーションの扱いを抽出しました。なんと魔法のように役立つでしょう。
サイズが 0 の型をアロケートする
では、アロケータの API がサイズ 0 の型のアロケーションに対応していないのならば、
一体全体何を、アロケーションとして保存すればいいのでしょうか? そうさ,勿論 heap::EMPTY
さ!
ZST に対する操作は、 ZSTがちょうど 1 つの値を持つため、 ほとんど全てが no-op となります。
それゆえこの型の値を保存したりロードしたりする場合に、状態を考える必要がありません。
この考えは実際に ptr::read
や ptr::write
に拡張されます。つまり、これらの操作は、
実際には全くポインタに着目していないのです。ですからポインタを変える必要は全くないのです。
ですが、サイズが 0 の型に対しては、オーバーフローの前にメモリ不足になる、という 前述した前提は最早有効ではないということに注意してください。サイズが 0 の型に対しては、 キャパシティのオーバーフローに対して明示的にガードしなければなりません。
現在のアーキテクチャでは、これは RawVec の各メソッド内に一つずつ、合わせて 3 つの ガードを書くことを意味します。
impl<T> RawVec<T> {
fn new() -> Self {
unsafe {
// !0 は usize::MAX です。この分岐はコンパイル時に取り除かれるはずです。
let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 };
// heap::EMPTY は "アロケートされていない" と "サイズが 0 の型のアロケーション" の
// 2 つの意味を兼ねることになります。
RawVec { ptr: Unique::new(heap::EMPTY as *mut T), cap: cap }
}
}
fn grow(&mut self) {
unsafe {
let elem_size = mem::size_of::<T>();
// elem_size が 0 の時にキャパシティを usize::MAX にしたので、
// ここにたどり着いてしまうということは、 Vec が満杯であることを必然的に
// 意味します。
assert!(elem_size != 0, "capacity overflow");
let align = mem::align_of::<T>();
let (new_cap, ptr) = if self.cap == 0 {
let ptr = heap::allocate(elem_size, align);
(1, ptr)
} else {
let new_cap = 2 * self.cap;
let ptr = heap::reallocate(*self.ptr as *mut _,
self.cap * elem_size,
new_cap * elem_size,
align);
(new_cap, ptr)
};
// もしアロケートや、リアロケートに失敗すると、 `null` が返ってきます
if ptr.is_null() { oom() }
self.ptr = Unique::new(ptr as *mut _);
self.cap = new_cap;
}
}
}
impl<T> Drop for RawVec<T> {
fn drop(&mut self) {
let elem_size = mem::size_of::<T>();
// サイズが 0 の型のアロケーションは解放しません。そもそもアロケートされていないからです。
if self.cap != 0 && elem_size != 0 {
let align = mem::align_of::<T>();
let num_bytes = elem_size * self.cap;
unsafe {
heap::deallocate(*self.ptr as *mut _, num_bytes, align);
}
}
}
}
以上。これで、サイズが 0 の型に対するプッシュとポップがサポートされます。 それでも (スライスの Deref から提供されていない) イテレータは、 まだ壊れているのですが。
サイズが 0 の型のイテレーション
サイズが 0 の型に対するオフセットは no-op です。つまり、現在の設計では start
と end
を
常に同じ値に初期化し、イテレータは何も値を返しません。これに対する今の所の解決策は、
ポインタを整数にキャストし、インクリメントした後に元に戻すという方法です。
impl<T> RawValIter<T> {
unsafe fn new(slice: &[T]) -> Self {
RawValIter {
start: slice.as_ptr(),
end: if mem::size_of::<T>() == 0 {
((slice.as_ptr() as usize) + slice.len()) as *const _
} else if slice.len() == 0 {
slice.as_ptr()
} else {
slice.as_ptr().offset(slice.len() as isize)
}
}
}
}
さて、これにより別のバグが発生します。イテレータが全く動作しない代わりに、 このイテレータは永遠に動き続けてしまいます。同じトリックをイテレータの impl に 行なう必要があります。また、 size_hint の計算では、 ZST の場合 0 で割ることになります。 基本的に 2 つのポインタを、それらがバイトサイズの値を指しているとして扱っているため、 サイズが 0 の場合、 1 で割ります。
impl<T> Iterator for RawValIter<T> {
type Item = T;
fn next(&mut self) -> Option<T> {
if self.start == self.end {
None
} else {
unsafe {
let result = ptr::read(self.start);
self.start = if mem::size_of::<T>() == 0 {
(self.start as usize + 1) as *const _
} else {
self.start.offset(1)
};
Some(result)
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let elem_size = mem::size_of::<T>();
let len = (self.end as usize - self.start as usize)
/ if elem_size == 0 { 1 } else { elem_size };
(len, Some(len))
}
}
impl<T> DoubleEndedIterator for RawValIter<T> {
fn next_back(&mut self) -> Option<T> {
if self.start == self.end {
None
} else {
unsafe {
self.end = if mem::size_of::<T>() == 0 {
(self.end as usize - 1) as *const _
} else {
self.end.offset(-1)
};
Some(ptr::read(self.end))
}
}
}
}
出来ました。イテレーションが動作します!