% Обработка типов нулевого размера

Пришло время. Начнем бороться с чудищем, называемым типами нулевого размера. Безопасному Rust никогда не нужно волноваться об этом, а вот Vec очень интенсивно использует сырые указатели и сырое выделение места, именно которым и надо заботиться о ТНР. Надо быть осторожным в двух вещах:

  • API сырого распределения места вызовет неопределенное поведение при передаче 0 в качестве размера выделяемого места.
  • Сдвиги сырых указателей являются пустыми операциями для ТНР, что сломает наш Си-подобный итератор указателей.

К счастью, мы абстрагировали наши итераторы по указателям и обработку распределения места в RawValIter и RawVec заранее. Как неожиданно удобно получилось.

Размещение типов нулевого размера

Итак, если API аллокатора не поддерживает выделение памяти нулевого размера, что же нам хранить в нашей выделенной памяти? Ну что ж, heap::EMPTY, конечно! Почти любая операция с ТНР является пустой операцией из-за того, что у ТНР только одно значение, и, следовательно, не нужно учитывать никакое состояние ни при чтении, ни при записи значений таких типов. Это на самом деле распространяется на ptr::read и ptr::write: они вообще не смотрят на указатель. По сути, нам никогда не придётся менять указатель.

Заметим, однако, что мы больше не можем надеяться на возникновение нехватки памяти до переполнения в случае ТНР. Мы должны явно защититься от переполнения емкости для ТНР.

В нашей текущей архитектуре это означает написание 3 охраняющих условий, по одному в каждый метод RawVec.

impl<T> RawVec<T> {
    fn new() -> Self {
        unsafe {
            // !0 это usize::MAX. Эта ветка удалится во время компиляции.
            let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 };

            // heap::EMPTY служит как для "невыделения", так и для "выделения нулевого размера"
            RawVec { ptr: Unique::new(heap::EMPTY as *mut T), cap: cap }
        }
    }

    fn grow(&mut self) {
        unsafe {
            let elem_size = mem::size_of::<T>();

            // из-за того, что мы установили емкость в usize::MAX если elem_size равен
            // 0, то попадание сюда обозначает, что 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>();

        // не освобождаем выделения нулевого размера, потому что выделение никогда не происходило.
        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);
            }
        }
    }
}

Вот и все. Теперь мы добавили поддержку push и pop для ТНР. Хотя наши итераторы (не предоствляемые срезом Deref) все еще не работают.

Итерирование по типам нулевого размера

Смещения нулевого размера являются пустыми операциями. Это означает, что в нашей текущей архитектуре мы всегда будем инициализировать 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)
            }
        }
    }
}

Теперь у нас другая ошибка. Раньше наши итераторы вообще не запускались, теперь они выполняются вечно. Необходимо сделать тот же трюк в реализации итераторов. Также, наш код вычисления size_hint будет вызывать деление на 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))
            }
        }
    }
}

И все. Итерация работает!