TL;DR: Add #[repr(C)]
and you should be good.
There are two separate concerns here: Whether the transmute is valid in the sense of returning valid data at the return type, and whether the entire thing violates any higher-level invariants that might be attached to the involved types. (In the terminology of my blog post, you have to make sure that both validity and safety invariants are maintained.)
For the validity invariant, you are in uncharted territory. The compiler could decide to lay out Map<T, M>
very differently from Map<T, ()>
, i.e. the data
field could be at a different offset and there could be spurious padding. It does not seem likely, but so far we are guaranteeing very little here. Discussion about what we can and want to guarantee there is happening right now. We purposefully want to avoid making too many guarantees about repr(Rust)
to avoid painting ourselves into a corner.
What you could do is to add repr(C)
to your struct, then I am fairly sure you can count on ZSTs not changing anything (but I asked for clarification just to be sure). For repr(C)
we provide more guarantees about how the struct is laid out, which in fact is its entire purpose. If you want to play tricks with struct layout, you should probably add that attribute.
For the higher-level safety invariant, you must be careful not to create a broken Map
and let that "leak" beyond the borders of your API (into the surrounding safe code), i.e. you shouldn't return an instance of Map
that violates any invariants you might have put on it. Moreover, PhantomData
has some effects on variance and the drop checker that you should be aware of. With the types that are being transmuted being so trivial (your marker types don't require dropping, i.e. them and their transitive fields all do not implement Drop
) I do not think you have to expect any problem from this side.
To be clear, repr(Rust)
(the default) might also be fine once we decide this is something we want to guarantee -- and ignoring size-0-align-1 types (like PhantomData
) entirely seems like a pretty sensible guarantee to me. Personally though I'd still advise for using repr(C)
unless that has a cost you are not willing to pay (e.g. because you lose the compilers automatic size-reduction-by-reordering and cannot replicate it manually).
fn(M) -> M
has static lifetime and does not implementDrop
, transmutingPhantomData<fn(M) -> M>
toPhantomData<()>
shouldn't have any observable effect except for type checking. – Mardellmardenstruct PlayerMapMarker(String)
could not be added to aMap<ItemMapMarker>
. – Smearcase