在尝试重用具有终生性的 Vec 时如何取悦借用检查器

How to please the borrow checker when trying to reuse a Vec with a lifetime

提问人:ChrisB 提问时间:10/14/2023 最后编辑:ChrisB 更新时间:10/15/2023 访问量:139

问:

在 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::foosFooprocess()

FooProcessor通常是一个大的、长寿命的对象(具体来说,它的寿命可能比某些 s 长),我不想每次调用都重新分配(甚至不使用某些东西)。FooVec<&Foo>process()SmallVec

有没有一种很好的方法可以解决这个问题,每次我都有这个模式时都不需要使用不安全的代码(生存期嬗变/指针)? (如果解决方案涉及使用一些板条箱,那么如果该板条箱在内部使用不安全,当然也没关系)。

Rust Lifetime 借用检查器

评论

0赞 cdhowie 10/14/2023
您可以使用一个包装器来完成这项工作,该包装器可以通过强制每次生命周期更改时强制清除内部来安全地转换生命周期。然后,您可以将其与生命周期一起存储在可能位于 .这将限制包装器的使用,这意味着您不需要单独使用。Vec<*mut Foo>Vec'staticfoosOptionunsafeunsafeFooProcessor
0赞 ChrisB 10/14/2023
@cdhowie:我考虑过这样做。问题是,有时我需要一个代替(来自传递为 的 s),然后这又不起作用了。所以我认为这些东西可能有一个通用的解决方案/板条箱,并想问问社区。Vec<Foo<'a>>'aFooinput
0赞 cdhowie 10/14/2023
嗯,好吧,每个 UCG 都有相同的布局,所以你可以转换。我正在尝试围绕转换操作提出一个安全的包装器。Foo<'a>Foo<'b>
0赞 cdhowie 10/14/2023
我遇到的问题是一般地表达操作,因为 Rust 没有像 C++ 的模板模板那样的高级概念。
0赞 ChrisB 10/14/2023
@cdhowie:如果你(或任何人)能想出你所描述的东西,我会很高兴。只是要保持开放,希望:)

答:

5赞 cdhowie 10/14/2023 #1

转换为通常是不健全的。但是,我们可以使用 Vec::into_raw_parts 将向量分解为数据指针、长度和容量,转换数据指针,然后使用 Vec:from_raw_parts 重新创建具有新类型的向量,只要我们能满足 的不变量。Vec<T>Vec<U>Vec::from_raw_parts

好吧,我们可以......但不稳定。但是,它是根据公共方法实现的,因此复制和粘贴实现是微不足道的:Vec::into_raw_partsVec

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需要与分配的内容具有相同的对齐方式。(不那么严格的对齐是不够的,对齐确实需要相等才能满足必须以相同的布局分配和解除分配内存的要求。ptrTdealloc

Vec将确保分配对齐,因此我们可以让我们的函数断言并具有相同的对齐方式。由于类型在编译时是已知的,因此如果通过,编译器将省略此检查。TTU

的大小乘以 (即分配的大小(以字节为单位)的大小需要与指针分配的大小相同。(因为与对齐类似,所以必须用相同的布局调用。Tcapacitydeallocsize

我们还将断言 和 的大小相等,这满足了这个不变量。TU

length需要小于或等于容量。

capacity需要是分配指针的容量。

分配的大小(以字节为单位)不得大于 isize::MAX。请参阅 pointer::offset 的安全文档。

我们将从现有向量中获取长度、容量和指针,除非我们已经在某处命中了一些 UB,否则它必须满足这些不变量。

第一个值必须是 类型的正确初始化值。lengthT

我们将在分解向量之前清除它,因此将为零,从而空洞地满足这个不变性。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) }
}

要利用这一点,您可以存储为 .在 中 ,您可以窃取分配,留下一个零容量向量,使用 转换生命周期,使用向量进行工作,将生存期转换回 ,并将其存储回 中。foosVec<&'static mut T>process()std::mem::take(&mut self.foos)safe_vec_transmute'staticself.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

评论

1赞 kmdreko 10/14/2023
“当参数具有相同的布局时,您可以在 Vec 类型之间定义安全嬗变” - 不完全是一般情况,请参阅:如何在不复制向量的情况下将 Vec<T> 转换为 Vec<U>?自身的布局可能会更改,但在这种情况下不太可能更改,因为只有生存期参数会更改。Vec
1赞 cdhowie 10/15/2023
@kmdreko对。无论哪种方式,我认为该函数无论如何都会编译为无操作,因此我将更新我的答案以使用更安全的方法。
1赞 cdhowie 10/15/2023
@ChrisB 注意:我已经更新了我的答案,以考虑 kmdreko 的反馈。
1赞 ChrisB 10/15/2023
@cdhowie:这很美,和我想象的一模一样。感谢您为此付出的所有努力!作为最后的润色,我添加了一个方便的包装器,现在很高兴:)。fn temp_vec(&mut Vec<T>, impl FnOnce(&mut Vec<U>))
2赞 user4815162342 10/15/2023
此外,您可以将检查提升到编译时:play.rust-lang.org/...