处理零尺寸类型

是时候了。我们要来对付零尺寸类型这个幽灵了。安全的 Rust 代码永远不需要关心这个,但是 Vec 对原始指针和原始分配的使用非常密集,而这正是关心零尺寸类型的两件事。我们需要小心两件事

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

幸运的是,我们分别将指针迭代器和分配处理抽象成了 RawValIterRawVec。真是神秘的方便。

分配零尺寸类型

所以,如果分配器 API 不支持零尺寸分配,我们到底要存储什么作为我们的分配呢?当然是 NonNull::dangling()!几乎所有对 ZST 的操作都是无操作的,因为 ZST 只有一个值,因此不需要考虑任何状态来存储或加载它们。这实际上也扩展到了 ptr::readptr::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 提供的)仍然有问题。

迭代零尺寸类型

零尺寸偏移量是无操作的。这意味着我们当前的设计将始终将 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().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))
                }
            }
        }
    }
}

就是这样。迭代工作正常!