Variance subtyping and understanding const vs mut pointers

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 Foo is 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.

It sounds like you would find these useful:

  1. Crust of Rust: Subtyping and Variance
  2. Crust of Rust: The Drop Check

Regarding the two pointer types their only difference are which operations require a cast to the other kind, which don't require a cast, and their variance. The kind of operations you are allowed to perform on a raw pointer has nothing to do with which kind of raw pointer it is, rather, it is determined by the kind of reference the raw pointer was originally created from.

3 Likes

Thanks I will give these a look :slight_smile:

Or, in textual form, and in this very forum:

  • I've written a shorter version of that post (focusing on variance exclusively) here

The TL,DRs:

  • Variance (and subtyping) is related to "being able to shrink lifetime( parameter)s" – or in some cases (contravariance), expanding lifetime( parameter)s.

  • The interaction between PhantomData and dropck only matters when an unsafe impl<#[may_dangle] …> is used, so it's more of a theoretical curiosity more than a knowledge with actual practical usages :grinning_face_with_smiling_eyes:

3 Likes

I am enjoying the Crust of Rust series, thank you for the recommendation @alice

1 Like

Forgot to say thanks @alice, the crust of rust strtok example is exactly what I needed.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.