Differences between *const T
and *mut T
The main difference between mutable and const raw pointer is, not surprisingly, whether dereferencing them yields a mutable or immutable place expression. Dereferencing a const pointer yields an immutable place expression, dereferencing a mutable pointer yields a mutable one. The implications of mutability according to the language reference are this:
For a place expression to be assigned to, mutably borrowed, implicitly mutably borrowed, or bound to a pattern containing ref mut
it must be mutable.
The other difference between const and mutable pointers is the variance of the types, as you already noted, and I think that's all there is.
Casting between mutable and const pointers
You can cast a *const T
to a *mut T
in safe code, since the difference in mutability only becomes relevant once you dereference the pointers, and dereferencing a raw pointer is an unsafe operation anyway. Without casting to a mutable pointer, you cannot get a mutable place expression for the memory a const pointer points to.
One reason Rust can be a bit more relaxed about mutability for raw pointers is that it does not make any assumptions about aliasing for raw pointers, in contrast to references. See What are the semantics for dereferencing raw pointers? for further details.
Why is NonNull
using *const T
?
The NonNull
pointer type is used as a building block for smart pointers like Box
and Rc
. These types expose interfaces that follow the usual Rust rules for references – mutation of the pointee is only possible through ownership of or a mutable reference to the smart pointer itself, and a shared reference to the pointee can only be obtained by borrowing the smart pointer itself. This means it is safe for these types to be covariant, which is only possible if NonNull
is covariant, which in turn means we need to use a *const T
rather than a *mut T
.
Why does the language include two different kinds of pointers if they are so similar?
Let's think about the alternative. If there was only a single pointer type, it would necessarily need to be the mutable pointer – otherwise we'd be unable to modify anything through a raw pointer. But that pointer type would also need to be covariant, since otherwise we'd be unable to build covariant smart pointer types. (It's always possible to give up covariance by including a PhantomData<some invariant type>
in a struct, but once your struct is rendered invariant by one of its members, there is no way to make it covariant again.) Since mutable references are invariant, the behaviour of this imaginary pointer type would be somewhat surprising.
Having two different pointer types, on the other hand, allows for a nice analogy to references: const pointers are covariant and dereference to immutable place expressions, just like shared references, and mutable pointers are invariant and dereference to mutable place expressions, just like mutable references.
I can only speculate whether these were the actual reasons for the design of the language, since I could not find any discussion on the topic, but the decision doesn't seem unreasonable to me.
*const T
. – Westleigh