Why do we need two kinds of raw pointers?

In C, const pointers are the only way of giving immutable access to a memory buffer. In Rust we have convenient things like references (immutable (shared) and mutable (unique)), so nobody really uses pointers (really rarely and only for FFI and some unsafe focuses). So. My offer is to deprecate *const T and *mut T and replace them with mutable T*. Why?

  1. *const pointers can be safely casted to *mut;
  2. *mut pointers, unlike &mut references, are not unique.

Probable troubles:

  1. Problems with rewriting low-level legacy.

Probable solutions:

  1. Make T* an unstable feature and deprecate *const T and *mut T after it's stabilization;
  2. Make *const T and *mut T equal to T*.

I would vote for &raw T rather than T*. It's confusing that raw pointer types use * rather than &.

Well, you're notice we only have NonNull in std::ptr - Rust, not NonNullConst and NonNullMut.

(But I think there's some super-unclear variance implications?)

1 Like

While it's sound to cast a *const T to a *mut T, it's rarely safe to use that pointer mutably. Since *const T is almost always derived from a shared reference, it's almost always undefined behavior to mutate the pointed to value. Additionally, there are variance implications, since *const T is covariant over T while *mut T is invariant (NonNull<T> is also covariant). Generally speaking, *mut T represents a pointer to a mutable object and *const T represents a pointer to an immutable one. Removing one would decrease the clarity of unsafe code, and lead to lots of subtle variance related soundness bugs, and lots of less subtle "Wait I wasn't supposed to mutate with that pointer?" bugs.

13 Likes

Famously, Rust 1.15.1 was needed in part because of casting a const to a mut: https://blog.rust-lang.org/2017/02/09/Rust-1.15.1.html#whats-in-1151-stable.

7 Likes

While this is true, having a const vs mut distinction is helpful for tracking provenance information around mutability, particularly in cases where there's a low-level (e.g. -sys) API that takes pointers and you're writing a higher-level binding that exposes a safe API and are converting & vs &mut references to pointers in the process.

And as others have mentioned, they differ in variance. You might want to peruse this thread:

Note that that information is partially outdated. At least for now (until all the detailed rules are figured out), *const vs *mut matters on reference to raw-pointer casts. This is UB:

let mut x = 0;
let ptr = &mut x as *const i32; // notice the 'const'!
(ptr as *mut i32).write(42);

It is UB because a raw pointer initially created by as *const _ is read-only, even if it is created from a mutable reference. These might not be the rules we want forever, but it matches how the borrow checker treats reference-to-raw-pointer casts, so these are the rules we are going with for now.

1 Like

That's a rather confusing rule. If it stays, it would be nice to have a deny lint or error which forbids &mut T as *const T casts entirely. If one wants a *const T, it is much more clear to cast via a &T, i.e. &mut T as &T as *const T. Alternatively, cast &mut T as *mut T as *const T, if mutability is required.

In fact, it would be nice to forbid casting both mutability and pointer at the same time, regardless whether the rule stays, to avoid such ambiguities.

2 Likes

Yeah I am not saying it's a great rule, it is just what we have right now, and if we want to change it we probably want to also change the borrow checker.

The thing is that this isn't what's happening, IIUC. Rather, semantically &mut T as *const T is actually AsCast[ Coerce[ &mut T, &T ], *const T ]. Or at least it was at one point, anyway.

2 Likes

How does that rule work with NonNull? NonNull is defined as struct NonNull { pointer: *const T } but is supposed to be used as a mutable pointer.

reference as NonNull<_> is not valid so I don't understand the question. NonNull is just a library type, it doesn't have any special rules (except for its niche optimizations).

What I meant was why is the example above UB when the below is normal use of NonNull and also goes via a const pointer:

let mut x = 0;
let ptr = NonNull::new_unchecked(&mut x);
(ptr.as_ptr()).write(42);

However re-reading your comment and NonNull::new_unchecked, it looks like the above is OK because &mut x gets coerced to *mut i32 and only then gets cast to *const i32, so it's not "initially created by as *const _".

Exactly.

1 Like