是否可以安全地转换PhantomData标记?

orl*_*rlp 7 unsafe rust

这是脱离上下文所以它看起来有点奇怪,但我有以下数据结构:

use std::marker::PhantomData;

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

Map是一个关联映射,其中键被"标记"以防止在另一个不相关的映射上使用来自一个映射的键.用户可以通过传递他们制作的一些独特类型来选择M,例如:

struct PlayerMapMarker;
let mut player_map: Map<String, PlayerMapMarker> = Map::new();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

struct Values<'a, T> {
    inner: Iter<'a, T, ()>,
}
Run Code Online (Sandbox Code Playgroud)

Ral*_*ung 5

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


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

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

你可以做的就是添加repr(C)到你的结构中,然后我相当确定你可以指望 ZST 不会改变任何东西(但我要求澄清只是为了确定)。因为repr(C)我们对结构的布局方式提供了更多保证,这实际上就是它的全部目的。如果您想玩弄结构布局,您可能应该添加该属性。

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

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