Rust 处理零尺寸类型
安全Rust并不需要关心零尺寸类型,但是Vec大量的依赖裸指针和内存分配,这些都需要零尺寸类型。我们要小心两件事情:
- 当给分配器API传递分配尺寸为0时,会导致未定义行为
- 对零尺寸类型的裸指针做offset是一个no-op,这会破坏我们的C-style指针迭代器。
幸好我们把指针迭代器和内存分配逻辑抽象出来放在RawValIter和RawVec中了。真是太方便了。
为零尺寸类型分配空间
如果分配器API不支持分配大小为0的空间,那么我们究竟储存了些什么呢?当然是Unique::empty()
了!基本上所有关于ZST的操作都是no-op,因为ZST只有一个值,不需要储存或加载任何的状态。这也同样适用于ptr::read
和ptr::write
:它们根本不会看那个指针一眼。所以我们并不需要修改指针。
注意,我们之前的分配代码依赖于OOM会先于数值溢出出现的假设,对于零尺寸类型不再有效了。我们必须显式地保证cap的值在ZST的情况下不会溢出。
基于现在的架构,我们需要写3处保护代码,RawVec的三个方法每个都有一处。
impl<T> RawVec<T> { fn new() -> Self { // !0就是usize::MAX。这段分支代码在编译期就可以计算出结果。 let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 }; // Unique::empty()有着“未分配”和“零尺寸分配”的双重含义 RawVec { ptr: Unique::empty(), cap: cap } } fn grow(&mut self) { unsafe { let elem_size = mem::size_of::<T>(); // 因为当elem_size为0时我们设置了cap为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_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_ptr() as *mut _, num_bytes, align); } } } }
就是这样。我们现在已经支持push和pop零尺寸类型了。但是迭代器(slice未提供的)还不能工作。
迭代零尺寸类型
offset 0是一个no-op。这意味着我们的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) } } } }
现在我们有了一个新的bug。我们成功地让迭代器从完全不运行,变成了永远不停地运行。我们需要在迭代器的实现中玩同样的把戏。同时,size_hint
在ZST的情况下会出现除数为0的问题。因为我们假设这两个指针都指向某个字节,我们在除数为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)) } } } }
很好,迭代器也可以工作了。
#![feature(ptr_internals)]#![feature(allocator_api)]#![feature(alloc_layout_extra)]use std::ptr::{Unique, NonN ...