サイズが 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::readptr::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 です。つまり、現在の設計では startend を 常に同じ値に初期化し、イテレータは何も値を返しません。これに対する今の所の解決策は、 ポインタを整数にキャストし、インクリメントした後に元に戻すという方法です。

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))
            }
        }
    }
}

出来ました。イテレーションが動作します!

関連キーワード:  self, サイズ, size, ptr, cap, let, elem, new, mem, else