I sometimes use types that simply serve as a marker when passed as type arguments to other types. A recent example in real life code, where I used this, is mmtkvdb::KeysUnique
and KeysDuplicate
.
In the above case, I used unit-like structs. They are types which have exactly one value, thus carry no information. These are zero-sized types, thus it would be possible to include them in another struct, and they would not consume any memory.
An alternative would be to use zero-variant enums. These types have zero values, thus they can never be instantiated. You could say they are zero-sized too (and apparently they are in Rust), though I prefer to think of them as being of undefined size (or having a size of −∞).
If I use zero-variant enums, then I can not include them like this in a struct:
struct SomeType<M> {
marker: M,
/* … */
}
Instead, I would need to use PhantomData
:
struct SomeType<M> {
marker: PhantomData<M>,
/* … */
}
If I use PhantomData
, I could use either unit-like structs or zero-variant enums.
But what is best?
- unit-like structs without
PhantomData
- unit-like structs with
PhantomData
- zero-variant enums with
PhantomData
I compiled a Playground example below to show the different variants and to share some observations I made:
use std::marker::PhantomData;
// We could use unit-like structs:
#[derive(Clone, Copy)]
pub struct Unit1;
#[derive(Clone, Copy)]
pub struct Unit2;
// Or we could use zero-variant enums:
pub enum Never1 {}
pub enum Never2 {}
// If we use unit-like structs, we don't need PhantomData:
pub struct Unitized<M> {
marker: M,
payload: i32,
}
// But then we might need extra bounds in a couple of places:
impl<M> Clone for Unitized<M>
where
M: Clone, // we need this bound in a lot of places :-(
//M: Default, // or this one
{
fn clone(&self) -> Self {
Unitized {
marker: self.marker.clone(),
//marker: Default::default(), // or this
payload: self.payload.clone(),
}
}
}
// Since we don't really use `M` for anything but marking,
// we could also make `marker` a `PhantomData`:
pub struct Phantomized<M> {
marker: PhantomData<M>,
payload: i32,
}
// That makes things easier:
impl<M> Clone for Phantomized<M> {
fn clone(&self) -> Self {
Phantomized {
marker: PhantomData,
payload: self.payload.clone(),
}
}
}
fn main() {
// Without `PhantomData`, we must use the unit-like approach:
let _unitized = Unitized {
marker: Unit1,
payload: 0,
};
// With `PhantomData` we could either use the unit-like or
// or the zero-variant enums:
let _phantomized_with_unit = Phantomized::<Unit1> {
marker: PhantomData,
payload: 0,
};
let _phantomized_with_never = Phantomized::<Never1> {
marker: PhantomData,
payload: 0,
};
}
In my real-life code, I currently use unit-like structs plus PhantomData
, see mmtkvdb::Db
, which corresponds to the variant _phantomized_with_unit
in the above Playground.
My question is: Is this a matter of taste? Are there reasons to prefer one of the approaches over the others? What do you think of it? What's idiomatic?