我有一个例程,它接受一个不可变的点切片,并执行一些转换,写入一个可变的点切片。(作为一个简单的例子,让我们假设我们用某个常数转换所有的点。)假设实现不能简化为逐点处理。(在我的例子中,我在确认编译器不能自动完成之后,对算法进行了向量化。)
在尽量减少重复代码的同时,我希望例程能够同时处理1.将一个数组转换为另一个数组,以及2.就地转换单个可变数组。
假设现有的实现具有以下形式
fn transform(in_: &[Point], out: &mut [Point], translate: Point) {
// ...
}
字符串
这解决了case(1),但是因为我们不能同时以可变和不变的方式借用同一个数组,所以这对case(2)不起作用。实际上,尝试使用unsafe,并同时获得可变和不可变的引用,是即时未定义的行为。
我们怎么才能绕过这个问题呢?我们能通过做一些像这样的事情来绕过规则吗?
unsafe fn transform_impl(in_: &[UnsafeCell<Point>], out: &[UnsafeCell<Point>], translate: Point) {
// ... promise we won't modify in_ here, nor have overlapping lifetimes
// between values read from in_ and written to out ...
}
fn transform_in_place(inout: &mut [Point], translate: Point) {
unsafe {
let a: &[UnsafeCell<Point>] = std::mem::transmute(inout);
transform_impl(a, a, translate);
}
}
fn transform(in_: &[Point], out: &mut [FPoint], translate: Point) {
unsafe {
transform_impl(std::mem::transmute(in_), std::mem::transmute(out), translate);
}
}
型
使用内部可变性,并保证我们不会从in_
中的值的“下面”更改out
中的值?或者像以前一样,将不可变切片转换为UnsafeCell切片,即时未定义行为?
(In C中,我们可以只使用指针,因为相同类型的非restrict
指针可能会别名。
我更倾向于用一个函数来处理这两种情况,如果只是为了最小化重复生成的代码量。如果这最终是不切实际的,什么是一个优雅的方法来使它工作而不需要在源代码中实现算法两次?
3条答案
按热度按时间pb3s4cty1#
考虑使用traits来处理这种情况。
你可以创建一个trait来定义“点I/O”,并使用方法来获取不可变和可变切片。然后你可以为
&mut [Point]
(就地情况)和(&[Point], &mut [Point])
(非重叠情况)实现它。trait不需要是公共的,因为你可以提供不同的公共函数来处理每种情况,但是委托给一个公共的实现函数。
这就要求实现的编写方式不需要读操作与写操作重叠。
字符串
jyztefdp2#
将
&mut T
转换为&UnsafeCell<T>
(因此&mut [Point]
到&[UnsafeCell<Point>]
绝对没问题。这是有文档记录的。你必须小心不要同时持有可变和共享引用,虽然,这意味着限制从in_
访问&
或从out
访问&mut
,但是不能同时对同一个索引执行这两个操作。甚至可以安全地使用Cell
,使用Cell::from_mut()
和Cell::as_slice_of_cells()
(取决于您需要如何访问Point
)。问题是另一个类型转换,
&T
到&UnsafeCell<T>
。如果引用然后被写入,这无疑是UB。问题是当我们只创建引用而不写入时会发生什么。Stacked Borrows标记了这个UB。Tree Borrows(另一个可能的别名模型)接受这个。我的理解是人们希望允许这个,但是不清楚如何允许Stacked Borrows。
考虑到别名模型还没有决定,我建议避免这种模式。如果你使用原始点而不是引用,你可以做你想做的事情;尽管这意味着更不安全的代码。
e0bqpujr3#
作为替代方案,我建议不依赖transmute,而是定义
transform_inplace()
的所有细节,并为不需要就地进行转换的情况提供简单的适配器。字符串