One of the most exciting features for me of Rust as being able to write safe abstractions for unsafe implementations. Because of that I am probably writing a lot more unsafe code than most Rust beginners.
I think I have managed to wrap my head around references versus pointers (references need to conform to the borrowing rules, always be aligned, always be backed by a valid bit pattern for their type), mutable references versus immutable references, MaybeUninit, UnsafeCell, that unsafe grants additional powers but still requires you to obey a bunch of rules, why drop doesn't run for statics, why thread locals don't allow escaping a closure, and at least some of pointer provenance...
But I still don't really understand variance and subtyping and whether I am always putting the type/lifetime I am supposed to inside PhantomData<T>. Here is what I understand so far:
-
rustc is going to yell at me if I have a generic type that doesn't contain fields that use all of the type parameters. So at the bare minimum
PhantomData<T>exists as a means to silence this lint from the compiler. -
rustc is going to assume my type satisifies an autotrait if all of its fields do, and I can do
PhantomData<SomethingNotThatAutotrait>as a hack to disable this. -
rustc is going to yell at me if I have a generic type with a lifetime parameter that is unused. Here
PhantomData<&'a T>gives you a way to tell the borrow checker that your type should be treated as if it had such a reference. Given that in unsafe code you may be producing custom handle/reference-like types, the usefulness of this is clear to me. -
I can read the dictionary definitions of covariance, contravariance, etc. and given an imaginary type hierarchy figure out which types would be considered covariant or contravariant.
-
I have heard that the only kind of subtyping that currently exists in rust is lifetimes.
&'static Foois a subtype of&'a Foo. So I understand this chapter about variance in the nomicon must have something to do with proper support of lifetimes.
I think in order to really grok it I need some realistic Rust examples showing what happens if you get it wrong versus get it right. Unfortunately the nomicon uses a contrived example that tries to pretend Rust has inheritance, so I have a hard time relating it to writing my own smart reference and pointer types.
-
What are the implications of picking a type with the wrong variance? Unsafety? If it only makes the compiler overly aggressive about rejecting code, what is an example of code getting rejected that is fixed by picking the right type for PhantomData variance?
-
What are examples for when you want each type of variance?
-
Somehow this also affects 'drop check'? Why does drop care about PhantomData at all? In C++, if Foo contains a Bar, then the Foo destructor executes the Bar destructor, and that fact can be opaque to the compiler when compiling functions that use Foo, as long as it inserts calls to the Foo destructors at the right place. Why is Rust different?
This post is already too long but for context part of the reason that I care is when writing abstractions for custom pointer types there is the issue of whether or not to have two versions of my custom pointer type to mirror the fact that raw pointers have const vs mut. On the one hand, my understanding is that this distinction mostly is a lint because you are allowed to convert between the two types, so when making my own custom pointer types I am tempted to be lazy and just assume mut so I don't have to have two of everything. On the other hand according to the subtyping page they differ in their variance, so I'm not sure if I am locking my users out of use cases by doing this.