转化 PhantomData 标记安全吗?

Is transmuting PhantomData markers safe?

这是out of context采取的,所以看起来有点奇怪,但我有以下数据结构:

use std::marker::PhantomData;

pub struct Map<T, M=()> {
    data: Vec<T>,
    _marker: PhantomData<fn(M) -> M>,
}

Map 是一个关联映射,其中键是 "marked" 以防止在另一个不相关的映射上使用一个映射中的键。用户可以通过传递一些他们制作的独特类型 M 来选择加入此功能,例如:

struct PlayerMapMarker;
let mut player_map: Map<String, PlayerMapMarker> = Map::new();

一切都很好,但是我想为这个地图编写的一些迭代器(例如只给出值的迭代器)在它们的类型中不包含标记。下面的转化可以安全地丢弃标记吗?

fn discard_marker<T, M>(map: &Map<T, M>) -> &Map<T, ()> {
    unsafe { std::mem::transmute(map) }
}

这样我就可以编写和使用:

fn values(&self) -> Values<T> {
    Values { inner: discard_marker(self).iter() }
}

struct Values<'a, T> {
    inner: Iter<'a, T, ()>,
}

TL;DR: 添加 #[repr(C)] 就可以了。


这里有两个不同的问题:从 return 在 return 类型的有效数据的意义上来说,转换是否有效,以及整个事情是否违反了任何更高级别的不变量可能附加到所涉及的类型。 (在我的 blog post 的术语中,您必须确保保持有效性和安全不变性。)

对于有效性不变量,您处于未知领域。编译器可能决定 Map<T, M> 的布局与 Map<T, ()> 非常不同,即 data 字段可能处于不同的偏移量并且可能存在虚假填充。这似乎不太可能,但到目前为止我们在这里保证很少。关于我们可以和想要保证什么的讨论 happening right now。我们有意避免对 repr(Rust) 做出太多保证,以免将自己逼入绝境。

你可以做的是将 repr(C) 添加到你的结构中,然后我相当确定你可以指望 ZST 不会改变任何东西(但我 asked for clarification 只是为了确定)。对于 repr(C) 我们提供了更多关于结构布局的保证,这实际上是它的全部目的。如果你想玩弄结构布局,你应该添加那个属性。

对于更高级别的安全不变量,你必须小心不要创建一个损坏的 Map 并让那个 "leak" 超出你的 API 的边界(进入周围的安全代码),即你不应该 return 一个 Map 的实例,它违反了你可能已经放在它上面的任何不变量。此外,PhantomData 对方差和掉落检查器有一些影响,您应该注意这一点。由于正在转换的类型是如此微不足道(您的标记类型不需要删除,即它们和它们的传递字段都没有实现 Drop),我认为您不必从这方面期待任何问题。

需要明确的是,一旦我们决定这是我们想要保证的东西,repr(Rust)(默认值)也可能没问题——并忽略 size-0-align-1 类型(比如 PhantomData) 对我来说完全是一个非常明智的保证。就我个人而言,我仍然建议使用 repr(C) 除非它有你不愿意支付的成本(例如,因为你失去了编译器通过重新排序自动减少大小并且无法手动复制它)。