I'm writing some unsafe Rust code so I need to know the exact differences between *const T
and *mut T
. I assumed that it's like &T
and &mut T
(i.e. you just can't mutate T
through &T
, period), but that doesn't seem to be the case!
For example, the pointer wrapper NonNull<T>
is defined as follows (source):
pub struct NonNull<T: ?Sized> {
pointer: *const T,
}
However, it's possible to obtain a *mut T
from this wrapper via as_ptr
, which is just defined as:
pub const fn as_ptr(self) -> *mut T {
self.pointer as *mut T
}
The function is not even marked as unsafe
! I am not allowed to cast from &T
to &mut T
(for a good reason!), but apparently casting pointers like that is fine.
The Nomicon mentions in the chapter about variance that *const T
and *mut T
differ in variance:
*const T
: covariant*mut T
: invariant
Is this the only difference between the pointer types? That would seem strange to me...
What exactly are the differences between the pointer types? Are there restrictions for *const T
that *mut T
doesn't have? If the differences are minimal: what are additional reasons to include both pointer types in the language?
Raw pointers ( *const and *mut ) Raw pointers are pointers without safety or liveness guarantees. Raw pointers are written as *const T or *mut T . For example *const i32 means a raw pointer to a 32-bit integer.
A raw pointer is a pointer whose lifetime isn't controlled by an encapsulating object, such as a smart pointer. A raw pointer can be assigned the address of another non-pointer variable, or it can be assigned a value of nullptr . A pointer that hasn't been assigned a value contains random data.
Rust has a number of different smart pointer types in its standard library, but there are two types that are extra-special. Much of Rust's safety comes from compile-time checks, but raw pointers don't have such guarantees, and are unsafe to use.
A pointer is a general concept for a variable that contains an address in memory. This address refers to, or “points at,” some other data. The most common kind of pointer in Rust is a reference, which you learned about in Chapter 4. References are indicated by the & symbol and borrow the value they point to.
*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.
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.
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
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With