Description
I hadn't seen this but it's very surprising and should be documented better. Apparently, *mut T
and *const T
aren't equivalent — if the raw pointer starts out as a *const T
it will always be illegal to write to, even nothing beyond the pointer's "memory" of its initial state is the reason for this.
See: rust-lang/rust-clippy#4774 (comment)
This is not entirely correct... something like &mut foo as *mut T as *const T as *mut T is entirely harmless. What is relevant is the initial cast, when a reference is turned to a raw pointer. I think of the pointer as "crossing into another domain", that of uncontrolled raw accesses. If that initial transition is a *const, then the entire "domain" gets marked as read-only (modulo UnsafeCell). The raw ptrs basically "remember" the way that the first raw ptr got created.
This is extremely surprising, as lots of documentation and common wisdom indicates that *const T
vs *mut T
are identical except as a sort of lint, and that the variance is different.
In fact, often having correct variance in your types often forces using *const
even for mutable data (hence NonNull uses const). The prevalence of this certainly helps contribute to programmers belief that there's no meaningful difference, you just have to be sure the data you write to is legal for you to write to.
A common case where this happens is if you write a helper method to return a pointer, you might write this just once for the *const T
case, and use it even if you're a &mut self
and need a *mut T
result. I wouldn't think twice about this, mostly because the myth they're equivalent is so widespread
Ralf's comment here even further propagates this myth, in a thread explicitly asking about the differences here... https://internals.rust-lang.org/t/what-is-the-real-difference-between-const-t-and-mut-t-raw-pointers/6127/18 :
I agree. *const T and *mut T are equivalent in terms of UB.
More broadly, nowhere in the thread does the 'once *const
, always *const
' behavior come up, just that you need to make sure that you maintain the normal rust rules (e.g. the rules which would apply had you started out with a *mut T
).
I looked and in none of Rust's reference material could I find any mention of behavior like this. This is very surprising, and I had been under the impression that optimising accesses to raw pointers wasn't beneficial enough for Rust to care strongly about them.
I also think this breaks a lot of existing unsafe code given how widespread the belief that they are equivalent is, and makes non-const-correct C libraries much thornier to bind to :(