提问人:ChrisB 提问时间:10/14/2023 最后编辑:ChrisB 更新时间:10/15/2023 访问量:139
在尝试重用具有终生性的 Vec 时如何取悦借用检查器
How to please the borrow checker when trying to reuse a Vec with a lifetime
问:
在 Rust 中,我遇到的一个常见模式是这样的:
struct Foo { /*...*/ }
struct FooProcessor {
foos: Vec<&'??? mut Foo>, // this lifetime is the issue, see explanation below
// other data structures needed for processing Foos ....
}
impl FooProcessor {
// just to make it clear, `self` may outlive `input` and it's `Foo`s
pub fn process(&mut self, input: &mut [Foo]) {
// some example operation that requires random access to a subset
self.foos.extend(input.iter().filter(|f| f.some_property()));
self.foos.sort();
// work some more on the Foos...
// remove all references to the processed Foos
self.foos.clear();
}
}
问题在于 的生存期,如上面的问号所强调的那样。所有对 s 的引用都在 的末尾被清除,但借用检查器当然不知道这一点。FooProcessor::foos
Foo
process()
FooProcessor
通常是一个大的、长寿命的对象(具体来说,它的寿命可能比某些 s 长),我不想每次调用都重新分配(甚至不使用某些东西)。Foo
Vec<&Foo>
process()
SmallVec
有没有一种很好的方法可以解决这个问题,每次我都有这个模式时都不需要使用不安全的代码(生存期嬗变/指针)? (如果解决方案涉及使用一些板条箱,那么如果该板条箱在内部使用不安全,当然也没关系)。
答:
转换为通常是不健全的。但是,我们可以使用 Vec::into_raw_parts 将向量分解为数据指针、长度和容量,转换数据指针,然后使用 Vec
::from_raw_parts
重新创建具有新类型的向量,只要我们能满足 的不变量。Vec<T>
Vec<U>
Vec::from_raw_parts
好吧,我们可以......但不稳定。但是,它是根据公共方法实现的,因此复制和粘贴实现是微不足道的:Vec::into_raw_parts
Vec
fn vec_into_raw_parts<T>(v: Vec<T>) -> (*mut T, usize, usize) {
let mut v = std::mem::ManuallyDrop::new(v);
(v.as_mut_ptr(), v.len(), v.capacity())
}
好了,现在让我们来看看 ' 不变量的列表:Vec::from_raw_parts
ptr
必须已使用全局分配器进行分配,例如通过函数进行分配。alloc::alloc
我们从使用全局分配器的 a 中获取指针。Vec
T
需要与分配的内容具有相同的对齐方式。(不那么严格的对齐是不够的,对齐确实需要相等才能满足必须以相同的布局分配和解除分配内存的要求。ptr
T
dealloc
Vec
将确保分配对齐,因此我们可以让我们的函数断言并具有相同的对齐方式。由于类型在编译时是已知的,因此如果通过,编译器将省略此检查。T
T
U
的大小乘以 (即分配的大小(以字节为单位)的大小需要与指针分配的大小相同。(因为与对齐类似,所以必须用相同的布局调用。
T
capacity
dealloc
size
我们还将断言 和 的大小相等,这满足了这个不变量。T
U
length
需要小于或等于容量。
capacity
需要是分配指针的容量。分配的大小(以字节为单位)不得大于 isize::MAX。请参阅 pointer::offset 的安全文档。
我们将从现有向量中获取长度、容量和指针,除非我们已经在某处命中了一些 UB,否则它必须满足这些不变量。
第一个值必须是 类型的正确初始化值。
length
T
我们将在分解向量之前清除它,因此将为零,从而空洞地满足这个不变性。length
现在,我们可以用以下术语定义元素类型之间的安全嬗变:Vec
fn safe_vec_transmute<T, U>(mut v: Vec<T>) -> Vec<U> {
assert_eq!(std::mem::size_of::<T>(), std::mem::size_of::<U>());
assert_eq!(std::mem::align_of::<T>(), std::mem::align_of::<U>());
v.clear();
let (ptr, len, cap) = vec_into_raw_parts(v);
// SAFETY:
//
// We assert T and U have the same size and alignment, and we clear the
// vector first. This satisfies several invariants of Vec:from_raw_parts.
// The remaining invariants are satisfied because we get ptr, len, and cap
// from an existing vector.
unsafe { Vec::from_raw_parts(ptr as *mut U, len, cap) }
}
要利用这一点,您可以存储为 .在 中 ,您可以窃取分配,留下一个零容量向量,使用 转换生命周期,使用向量进行工作,将生存期转换回 ,并将其存储回 中。foos
Vec<&'static mut T>
process()
std::mem::take(&mut self.foos)
safe_vec_transmute
'static
self.foos
它看起来像这样(未经测试的代码):
let foos = safe_vec_transmute(std::mem::take(&mut self.foos));
foos.extend(...);
foos.sort();
// More work
self.foos = safe_vec_transmute(foos);
请注意,编译(经过优化)的指令与清除向量并返回它的函数完全相同,因此运行时开销为零!safe_vec_transmute
评论
Vec
fn temp_vec(&mut Vec<T>, impl FnOnce(&mut Vec<U>))
评论
Vec<*mut Foo>
Vec
'static
foos
Option
unsafe
unsafe
FooProcessor
Vec<Foo<'a>>
'a
Foo
input
Foo<'a>
Foo<'b>