处理零尺寸类型

是时候了。我们要与零尺寸类型的幽灵作斗争。安全 Rust *永远*不需要关心这个问题,但是 Vec 对原始指针和原始分配非常密集,而这恰恰是关心零尺寸类型的两个方面。我们需要小心两件事:

  • 如果传递 0 作为分配大小,则原始分配器 API 具有未定义的行为。
  • 原始指针偏移对于零尺寸类型是空操作,这将破坏我们的 C 风格的指针迭代器。

值得庆幸的是,我们将指针迭代器和分配处理分别抽象到了 `RawValIter` 和 `RawVec` 中。这是多么神秘的方便。

分配零尺寸类型

因此,如果分配器 API 不支持零尺寸分配,那么我们到底将什么存储为我们的分配?当然是 `NonNull::dangling()`!ZST 的几乎所有操作都是空操作,因为 ZST 恰好只有一个值,因此无需考虑存储或加载它们的状态。这实际上扩展到了 `ptr::read` 和 `ptr::write`:它们根本不会查看指针。因此,我们永远不需要更改指针。

但是请注意,我们之前依赖于在溢出之前耗尽内存的做法对于零尺寸类型不再有效。我们必须明确地防止零尺寸类型的容量溢出。

由于我们当前的架构,所有这一切意味着要编写 3 个守卫,每个守卫都在 `RawVec` 的每个方法中。

impl<T> RawVec<T> {
    fn new() -> Self {
        // This branch should be stripped at compile time.
        let cap = if mem::size_of::<T>() == 0 { usize::MAX } else { 0 };

        // `NonNull::dangling()` doubles as "unallocated" and "zero-sized allocation"
        RawVec {
            ptr: NonNull::dangling(),
            cap,
        }
    }

    fn grow(&mut self) {
        // since we set the capacity to usize::MAX when T has size 0,
        // getting to here necessarily means the Vec is overfull.
        assert!(mem::size_of::<T>() != 0, "capacity overflow");

        let (new_cap, new_layout) = if self.cap == 0 {
            (1, Layout::array::<T>(1).unwrap())
        } else {
            // This can't overflow because we ensure self.cap <= isize::MAX.
            let new_cap = 2 * self.cap;

            // `Layout::array` checks that the number of bytes is <= usize::MAX,
            // but this is redundant since old_layout.size() <= isize::MAX,
            // so the `unwrap` should never fail.
            let new_layout = Layout::array::<T>(new_cap).unwrap();
            (new_cap, new_layout)
        };

        // Ensure that the new allocation doesn't exceed `isize::MAX` bytes.
        assert!(new_layout.size() <= isize::MAX as usize, "Allocation too large");

        let new_ptr = if self.cap == 0 {
            unsafe { alloc::alloc(new_layout) }
        } else {
            let old_layout = Layout::array::<T>(self.cap).unwrap();
            let old_ptr = self.ptr.as_ptr() as *mut u8;
            unsafe { alloc::realloc(old_ptr, old_layout, new_layout.size()) }
        };

        // If allocation fails, `new_ptr` will be null, in which case we abort.
        self.ptr = match NonNull::new(new_ptr as *mut T) {
            Some(p) => p,
            None => alloc::handle_alloc_error(new_layout),
        };
        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 {
            unsafe {
                alloc::dealloc(
                    self.ptr.as_ptr() as *mut u8,
                    Layout::array::<T>(self.cap).unwrap(),
                );
            }
        }
    }
}

就是这样。我们现在支持推送和弹出零尺寸类型。我们的迭代器(不是由切片 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().add(slice.len())
            },
        }
    }
}

现在我们有了一个不同的错误。我们的迭代器不是根本不运行,而是现在永远运行。我们需要在我们的迭代器实现中做同样的技巧。此外,我们的 size_hint 计算代码将为 ZST 除以 0。由于我们基本上会将两个指针视为指向字节,因此我们将大小 0 映射为除以 1。这是 `next` 的样子:

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

你看到“错误”了吗?其他人都没有!原作者在几年后链接到此页面时才注意到这个问题。这段代码有点可疑,因为滥用迭代器指针作为 *计数器* 会使它们不对齐!我们在使用 ZST 时的 *唯一任务* 是保持指针对齐!*捂脸*

原始指针不需要始终对齐,因此将指针用作计数器的基本技巧是 *可以的*,但是它们在传递给 `ptr::read` 时 *应该* 绝对对齐!这 *可能* 是不必要的吹毛求疵,因为 `ptr::read` 对于 ZST 是一个空操作,但是让我们稍微负责任一点,在 ZST 路径上从 `NonNull::dangling` 读取。

(或者,您可以在 ZST 路径上调用 `read_unaligned`。两者都可以,因为无论哪种方式,我们都是从无到有地创建一个值,并且所有这些都编译为不做任何事情。)

impl<T> Iterator for RawValIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<T> {
        if self.start == self.end {
            None
        } else {
            unsafe {
                if mem::size_of::<T>() == 0 {
                    self.start = (self.start as usize + 1) as *const _;
                    Some(ptr::read(NonNull::<T>::dangling().as_ptr()))
                } else {
                    let old_ptr = self.start;
                    self.start = self.start.offset(1);
                    Some(ptr::read(old_ptr))
                }
            }
        }
    }

    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 {
                if mem::size_of::<T>() == 0 {
                    self.end = (self.end as usize - 1) as *const _;
                    Some(ptr::read(NonNull::<T>::dangling().as_ptr()))
                } else {
                    self.end = self.end.offset(-1);
                    Some(ptr::read(self.end))
                }
            }
        }
    }
}

就是这样。迭代有效!