2 stable releases
2.0.1 | Jul 9, 2023 |
---|---|
1.0.3 |
|
#1648 in Rust patterns
11KB
Phtm
This crate provides type aliases for common usages of PhantomData
.
[dependencies]
phtm = "2.0.1"
lib.rs
:
Various verbose shorthand types related to PhantomData
.
Variance
Variance can be very confusing to beginners. Generally, when one talks about "subtype" and "supertype" in Rust, it is specifically about lifetimes in the current version.
Specifically, any reference type &'any T
is a subtype of
the static reference type &'static T
. A reference with a
shorter lifetime is a subtype of another reference with a
longer lifetime.
Covariant
When a Foo<T>
is "covariant" over T
, it shares the
same subtyping rules with T
, i.e. Foo<&'a T>
is a subtype
of Foo<&'static T>
.
When a lifetime parameter is covariant, it suggests that a
shorter lifetime parameter is a subtype of a longer lifetime
parameter, i.e. Foo<'a>
is a subtype of Foo<'static>
.
Contravariant
Contravariant is rare, it reverses the normal subtyping
rules. Foo<'static>
is a subtype of Foo<'a>
if the lifetimes
are contravariant. It is the property of argument types of
functions. fn(&'static ())
is the most strict and would be
the super type for all fn(&'a ())
.
Invariant
If a parameter is invariant, it cannot be changed. Only equal
arguments are sub/super types of themselves. T
is invariant
in &mut T
because you can perform both read and write
operations on it, and you cannot write a T2
with a shorter
lifetime than T
, nor read a T3
with a longer lifetime.
T
is invariant in fn(T) -> T
for similar reasons.
Drop Check
A difference between the CovariantOver
type offered in this
crate and the Owns
type is that Owns
will cause the compiler
to use "drop check". Drop checking prevents use of dropped values
in another type's destructor. The examples below are taken and
adapted from the respective Rustonomicon chapter:
Imagine a custom Box
type defined like this:
use std::ptr::NonNull;
struct MyBox<T> {
inner: NonNull<T>,
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) { /* free memory.. */ }
}
Although NonNull
is covariant over T
,
MyBox
must use PhantomData
to signify that it owns T
,
otherwise it could allow access to dropped data in a destructor:
struct Inspector<'a>(&'a u8);
impl<'a> Drop for Inspector<'a> {
fn drop(&mut self) {
println!("I was only {} days from retirement!", self.0);
}
}
struct World<'a> {
inspector: Option<MyBox<Inspector<'a>>>,
days: Box<u8>,
}
fn main() {
let mut world = World {
inspector: None,
days: Box::new(1),
};
world.inspector = Some(MyBox::new(Inspector(&world.days)));
// Let's say `days` happens to get dropped first.
// Then when Inspector is dropped, it will try to read free'd memory!
}
In this example, marking MyBox
with a PhantomData<T>
or Owns<T>
resolves the unsoundness issue:
struct MyBox<T> {
inner: NonNull<T>,
_owns_t: Owns<T>,
}
fn main() {
let mut world = World {
inspector: None,
days: Box::new(1),
};
world.inspector = Some(MyBox::new(Inspector(&world.days)));
// ^ now fails to compile!
}
Markers
The Send
and Sync
markers can be controlled by adding
NotSendOrSync
or NotSync
to your type and/or writing
unsafe impl
s of the markers to your type. The markers control
what types can be sent between threads safely. The non-atomic
reference counted type Rc
cannot be sent
between threads because there might be two threads holding the
same object and could increase the reference counter
non-atomically, causing a data race.
Usually your type is automatically not Send
when there are
fields of pointers or single-threaded containers such as Rc
,
but there could be benefits of explicitly defining a type as not
Send
or not Sync
.
Doing so can prevent semver hazards when a public type suddenly stops implementing any of these marker types, causing a breaking change, while explicitly adding the marker types allows future possibilities of having single-threaded containers without bumping the major version.