如何安全地存储通用值的不可变别名副本?
How to safely store immutable, aliasing copies of a generic value?
总结
出于优化目的,我希望创建一个数据结构,将单个值存储在多个不同的位置。数据结构只会让这些值通过不可变引用使用,或者将它们从数据结构中完全删除。
如何确保这对于泛型类型是安全的?
简化示例
为了给上下文举一个简单的(但有点不切实际)的例子,考虑一个缓存最近使用的值的切片:
struct Pair<'a> {
values: &'a [T],
last_accessed: &'a T,
}
然而,访问最后访问的元素仍然会导致指针取消引用,因此代码需要按值缓存:
struct Pair<'a> {
values: &'a [T],
last_accessed: NoDrop<T>,
}
在大多数情况下,这似乎是安全的。例如,如果 T
是 u32
,则缓存只是数据的简单副本。
即使 T
是 Vec<U>
,这似乎是安全的,因为通过 &last_accessed
的任何访问都不能更改向量的任何直接成员。堆分配是传递不可变且不重复的,因此不存在明显的别名问题。
为什么这么难?
这对 所有 值都是不安全的。包含 Cell
的值可能会调用内部可变性并最终违反内部约束,当通过未传播该更改的值访问时会导致不安全行为。
问题是我可以对通用 T
施加什么约束来确保安全?据我所知,我所需要的只是它不包含 UnsafeCell
除了通过指针。
T: Copy
呢?
T: Copy
看起来是一个不错的解决方案,但有两个主要缺点:
"there's no absolutely fundamental reason why [UnsafeCell
] does not implement Copy
" - Alex Crichton。 Copy
要求似乎是巧合而不是保证。
Copy
禁止太多; Vec
不是 Copy
但不需要被禁止。
那T: Sync
呢
Sync
接近正确的想法,因为
Types that are not Sync
are those that have "interior mutability" in a non-thread-safe way, such as Cell
and RefCell
in std::cell
.
但是,一些类型,如原子,具有内部可变性,是线程安全的。
您可以使用 auto trait 对 UnsafeCell
强制执行约束。这些是默认定义的,但可以使用特定类型的特殊语法选择退出 - 您只想为 UnsafeCell
.
选择退出
这些以前称为选择加入内置特征 (OIBIT),但已重命名,因为它们既不是选择加入也不一定是内置的,实际上是可以在普通语言中定义的选择退出特征用户代码。
首先启用它,创建特征并使其默认实现。这使用了一些神奇的语法。
#![feature(optin_builtin_traits)]
pub unsafe trait CopyRef {}
unsafe impl CopyRef for .. {}
然后您选择退出UnsafeCell
。
// Opt out of interior mutability
impl<T: ?Sized> !CopyRef for UnsafeCell<T> {}
然后您将需要重新启用指针后面的 UnsafeCell
和 PhantomData
。
use std::marker::PhantomData;
use std::cell::UnsafeCell;
// Opt in for indirect interior mutability
unsafe impl<'a, T: ?Sized> CopyRef for *const T {}
unsafe impl<'a, T: ?Sized> CopyRef for *mut T {}
unsafe impl<'a, T: ?Sized> CopyRef for &'a T {}
unsafe impl<'a, T: ?Sized> CopyRef for &'a mut T {}
// Box is special and needs its own opt-in
unsafe impl<T: ?Sized> CopyRef for Box<T> {}
// And fake interior mutability
unsafe impl<T: ?Sized> CopyRef for PhantomData<T> {}
瞧瞧。您自己的用户定义的选择退出选择内置自动特征。
总结
出于优化目的,我希望创建一个数据结构,将单个值存储在多个不同的位置。数据结构只会让这些值通过不可变引用使用,或者将它们从数据结构中完全删除。
如何确保这对于泛型类型是安全的?
简化示例
为了给上下文举一个简单的(但有点不切实际)的例子,考虑一个缓存最近使用的值的切片:
struct Pair<'a> {
values: &'a [T],
last_accessed: &'a T,
}
然而,访问最后访问的元素仍然会导致指针取消引用,因此代码需要按值缓存:
struct Pair<'a> {
values: &'a [T],
last_accessed: NoDrop<T>,
}
在大多数情况下,这似乎是安全的。例如,如果 T
是 u32
,则缓存只是数据的简单副本。
即使 T
是 Vec<U>
,这似乎是安全的,因为通过 &last_accessed
的任何访问都不能更改向量的任何直接成员。堆分配是传递不可变且不重复的,因此不存在明显的别名问题。
为什么这么难?
这对 所有 值都是不安全的。包含 Cell
的值可能会调用内部可变性并最终违反内部约束,当通过未传播该更改的值访问时会导致不安全行为。
问题是我可以对通用 T
施加什么约束来确保安全?据我所知,我所需要的只是它不包含 UnsafeCell
除了通过指针。
T: Copy
呢?
T: Copy
看起来是一个不错的解决方案,但有两个主要缺点:
"there's no absolutely fundamental reason why [
UnsafeCell
] does not implementCopy
" - Alex Crichton。Copy
要求似乎是巧合而不是保证。Copy
禁止太多;Vec
不是Copy
但不需要被禁止。
那T: Sync
呢
Sync
接近正确的想法,因为
Types that are not
Sync
are those that have "interior mutability" in a non-thread-safe way, such asCell
andRefCell
instd::cell
.
但是,一些类型,如原子,具有内部可变性,是线程安全的。
您可以使用 auto trait 对 UnsafeCell
强制执行约束。这些是默认定义的,但可以使用特定类型的特殊语法选择退出 - 您只想为 UnsafeCell
.
这些以前称为选择加入内置特征 (OIBIT),但已重命名,因为它们既不是选择加入也不一定是内置的,实际上是可以在普通语言中定义的选择退出特征用户代码。
首先启用它,创建特征并使其默认实现。这使用了一些神奇的语法。
#![feature(optin_builtin_traits)]
pub unsafe trait CopyRef {}
unsafe impl CopyRef for .. {}
然后您选择退出UnsafeCell
。
// Opt out of interior mutability
impl<T: ?Sized> !CopyRef for UnsafeCell<T> {}
然后您将需要重新启用指针后面的 UnsafeCell
和 PhantomData
。
use std::marker::PhantomData;
use std::cell::UnsafeCell;
// Opt in for indirect interior mutability
unsafe impl<'a, T: ?Sized> CopyRef for *const T {}
unsafe impl<'a, T: ?Sized> CopyRef for *mut T {}
unsafe impl<'a, T: ?Sized> CopyRef for &'a T {}
unsafe impl<'a, T: ?Sized> CopyRef for &'a mut T {}
// Box is special and needs its own opt-in
unsafe impl<T: ?Sized> CopyRef for Box<T> {}
// And fake interior mutability
unsafe impl<T: ?Sized> CopyRef for PhantomData<T> {}
瞧瞧。您自己的用户定义的选择退出选择内置自动特征。