原文链接:https://doc.rust-lang.org/nomicon/vec-zsts.html

处理零尺寸类型

是时候和零尺寸类型开战了。安全Rust并不需要关心这个,但是Vec大量的依赖裸指针和内存分配,这些都需要零尺寸类型。我们要小心两件事情:

  • 当给分配器API传递分配尺寸为0时,会导致未定义行为
  • 对零尺寸类型的裸指针做offset是一个no-op,这会破坏我们的C-style指针迭代器。

幸好我们把指针迭代器和内存分配逻辑抽象出来放在RawValIter和RawVec中了。真是太方便了。

为零尺寸类型分配空间

如果分配器API不支持分配大小为0的空间,那么我们究竟储存了些什么呢?当然是Unique::empty()了!基本上所有关于ZST的操作都是no-op,因为ZST只有一个值,不需要储存或加载任何的状态。这也同样适用于ptr::readptr::write:它们根本不会看那个指针一眼。所以我们并不需要修改指针。

注意,我们之前的分配代码依赖于OOM会先于数值溢出出现的假设,对于零尺寸类型不再有效了。我们必须显式地保证cap的值在ZST的情况下不会溢出。

基于现在的架构,我们需要写3处保护代码,RawVec的三个方法每个都有一处。

  1. impl<T> RawVec<T> {
  2. fn new() -> Self {
  3. // !0就是usize::MAX。这段分支代码在编译期就可以计算出结果。
  4. let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 };
  5. // Unique::empty()有着“未分配”和“零尺寸分配”的双重含义
  6. RawVec { ptr: Unique::empty(), cap: cap }
  7. }
  8. fn grow(&mut self) {
  9. unsafe {
  10. let elem_size = mem::size_of::<T>();
  11. // 因为当elem_size为0时我们设置了cap为usize::MAX,
  12. // 这一步成立意味着Vec的容量溢出了
  13. assert!(elem_size != 0, "capacity overflow");
  14. let align = mem::align_of::<T>();
  15. let (new_cap, ptr) = if self.cap == 0 {
  16. let ptr = heap::allocate(elem_size, align);
  17. (1, ptr)
  18. } else {
  19. let new_cap = 2 * self.cap;
  20. let ptr = heap::reallocate(self.ptr.as_ptr() as *mut _,
  21. self.cap * elem_size,
  22. new_cap * elem_size,
  23. align);
  24. (new_cap, ptr)
  25. };
  26. // 如果分配或再分配失败,我们会得到null
  27. if ptr.is_null() { oom() }
  28. self.ptr = Unique::new(ptr as *mut _);
  29. self.cap = new_cap;
  30. }
  31. }
  32. }
  33. impl<T> Drop for RawVec<T> {
  34. fn drop(&mut self) {
  35. let elem_size = mem::size_of::<T>();
  36. // 不要释放零尺寸空间,因为它根本就没有分配过
  37. if self.cap != 0 && elem_size != 0 {
  38. let align = mem::align_of::<T>();
  39. let num_bytes = elem_size * self.cap;
  40. unsafe {
  41. heap::deallocate(self.ptr.as_ptr() as *mut _, num_bytes, align);
  42. }
  43. }
  44. }
  45. }

就是这样。我们现在已经支持push和pop零尺寸类型了。但是迭代器(slice未提供的)还不能工作。

迭代零尺寸类型

offset 0是一个no-op。这意味着我们的startend总是会被初始化为相同的值,我们的迭代器也无法产生任何的东西。当前的解决方案是把指针转换为整数,增加他们的值,然后再转换回来:

  1. impl<T> RawValIter<T> {
  2. unsafe fn new(slice: &[T]) -> Self {
  3. RawValIter {
  4. start: slice.as_ptr(),
  5. end: if mem::size_of::<T>() == 0 {
  6. ((slice.as_ptr() as usize) + slice.len()) as *const _
  7. } else if slice.len() == 0 {
  8. slice.as_ptr()
  9. } else {
  10. slice.as_ptr().offset(slice.len() as isize)
  11. }
  12. }
  13. }
  14. }

现在我们有了一个新的bug。我们成功地让迭代器从完全不运行,变成了永远不停地运行。我们需要在迭代器的实现中玩同样的把戏。同时,size_hint在ZST的情况下会出现除数为0的问题。因为我们假设这两个指针都指向某个字节,我们在除数为0的情况下直接将除数变为1。

  1. impl<T> Iterator for RawValIter<T> {
  2. type Item = T;
  3. fn next(&mut self) -> Option<T> {
  4. if self.start == self.end {
  5. None
  6. } else {
  7. unsafe {
  8. let result = ptr::read(self.start);
  9. self.start = if mem::size_of::<T>() == 0 {
  10. (self.start as usize + 1) as *const _
  11. } else {
  12. self.start.offset(1)
  13. };
  14. Some(result)
  15. }
  16. }
  17. }
  18. fn size_hint(&self) -> (usize, Option<usize>) {
  19. let elem_size = mem::size_of::<T>();
  20. let len = (self.end as usize - self.start as usize)
  21. / if elem_size == 0 { 1 } else { elem_size };
  22. (len, Some(len))
  23. }
  24. }
  25. impl<T> DoubleEndedIterator for RawValIter<T> {
  26. fn next_back(&mut self) -> Option<T> {
  27. if self.start == self.end {
  28. None
  29. } else {
  30. unsafe {
  31. self.end = if mem::size_of::<T>() == 0 {
  32. (self.end as usize - 1) as *const _
  33. } else {
  34. self.end.offset(-1)
  35. };
  36. Some(ptr::read(self.end))
  37. }
  38. }
  39. }
  40. }

很好,迭代器也可以工作了。