转化PhantomData标记是安全的吗?

8

这段内容摘自 上下文,可能会有点奇怪,但我有以下数据结构:

use std::marker::PhantomData;

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

Map是一种关联映射,其中键被“标记”,以防止在另一个不相关的映射上使用同样的键。用户可以通过传递他们创建的某些唯一类型作为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, ()>,
}

由于 fn(M) -> M 具有静态生命周期并且没有实现 Drop,将 PhantomData<fn(M) -> M> 转换为 PhantomData<()> 不应该有任何可观察到的效果,除了类型检查。 - Sven Marnach
结构体中是否有多个非零大小的字段? - CodesInChaos
@CodesInChaos 在实际的结构体中,是的。 - orlp
这并没有回答问题,但从设计的角度来看,我更愿意将这些标记 newtype 化,而不是将其作为容器的逻辑。因此,一个 struct PlayerMapMarker(String) 的值不能被添加到 Map<ItemMapMarker> 中。 - E net4
正如我所说,如果没有上下文,这并没有太多意义。在真实的数据结构中,被标记的是,用户不能选择自己的键类型,它总是相同的。 - orlp
1个回答

5

TL;DR:添加#[repr(C)]即可。


这里有两个独立的问题:是否转换在返回类型上返回有效数据以及整个过程是否违反了可能与涉及类型相关联的更高级别的不变量。(按照我的博客帖子的术语,您必须确保维护有效性和安全不变量。)
对于有效性不变量,您处于未知领域。编译器可以决定非常不同于Map 的方式来布局Map,即data字段可以位于不同的偏移量,并且可能存在虚假填充。 这似乎不太可能,但到目前为止,我们几乎没有任何保证。讨论我们可以和想要在那里保证什么正在进行中。我们特意避免在repr(Rust)方面作出太多保证,以避免将自己画入角落。
你可以在结构体中添加repr(C),这样我相信你可以确保零大小类型不会引发任何变化(但我要求澄清以确保)。使用repr(C)提供了有关如何布局结构体的更多保证,事实上这是它的全部目的。如果要玩弄结构体布局的技巧,应该添加该属性。
对于更高级别的安全不变量,你必须小心不要创建一个破碎的Map并让它“泄漏”到API的边界之外(进入周围的安全代码),即不应返回违反任何不变量的Map实例。此外,PhantomData对协变和丢弃检查器有一些影响,你应该注意这一点。由于被转换的类型非常简单(你的标记类型不需要删除,即它们及其传递字段都不实现Drop),我认为你不必从这方面期望任何问题。
为了明确起见,“repr(Rust)”(默认值)一旦我们决定这是我们想要保证的东西,也可能很好,而完全忽略大小为0对齐为1的类型(如“PhantomData”)似乎对我来说是一个相当明智的保证。不过,个人建议仍然使用“repr(C)”,除非你不愿意支付成本(例如,因为你失去了编译器自动通过重新排序减小大小并且无法手动复制它)。

1
repr(transparent) 也应该是有效的,不是吗? - Marin Veršić
@MarinVeršić 是的 - Ralf Jung

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接