For shared references and mutable references the semantics are clear: as long as you have a shared reference to a value, nothing else must have mutable access, and a mutable
No awkward strict-aliasing here
C++ strict-aliasing is a patch on a wooden leg. C++ does not have any aliasing information, and the absence of aliasing information prevents a number of optimizations (as you noted here), therefore to regain some performance strict-aliasing was patched on...
Unfortunately, strict-aliasing is awkward in a systems language, because reinterpreting raw-memory is the essence of what systems language are designed to do.
And doubly unfortunately it does not enable that many optimizations. For example, copying from one array to another must assume that the arrays may overlap.
restrict
(from C) is a bit more helpful, although it only applies to one level at a time.
Instead, we have scope-based aliasing analysis
The essence of the aliasing analysis in Rust is based on lexical scopes (barring threads).
The beginner level explanation that you probably know is:
&T
, then there is no &mut T
to the same instance,&mut T
, then there is no &T
or &mut T
to the same instance.As suited to a beginner, it is a slightly abbreviated version. For example:
fn main() {
let mut i = 32;
let mut_ref = &mut i;
let x: &i32 = mut_ref;
println!("{}", x);
}
is perfectly fine, even though both a &mut i32
(mut_ref
) and a &i32
(x
) point to the same instance!
If you try to access mut_ref
after forming x
, however, the truth is unveiled:
fn main() {
let mut i = 32;
let mut_ref = &mut i;
let x: &i32 = mut_ref;
*mut_ref = 2;
println!("{}", x);
}
error[E0506]: cannot assign to `*mut_ref` because it is borrowed | 4 | let x: &i32 = mut_ref; | ------- borrow of `*mut_ref` occurs here 5 | *mut_ref = 2; | ^^^^^^^^^^^^ assignment to borrowed `*mut_ref` occurs here
So, it is fine to have both &mut T
and &T
pointing to the same memory location at the same time; however mutating through the &mut T
will be disabled for as long as the &T
exists.
In a sense, the &mut T
is temporarily downgraded to a &T
.
So, what of pointers?
First of all, let's review the reference:
- are not guaranteed to point to valid memory and are not even guaranteed to be non-NULL (unlike both
Box
and&
);- do not have any automatic clean-up, unlike
Box
, and so require manual resource management;- are plain-old-data, that is, they don't move ownership, again unlike
Box
, hence the Rust compiler cannot protect against bugs like use-after-free;- lack any form of lifetimes, unlike
&
, and so the compiler cannot reason about dangling pointers; and- have no guarantees about aliasing or mutability other than mutation not being allowed directly through a
*const T
.
Conspicuously absent is any rule forbidding from casting a *const T
to a *mut T
. That's normal, it's allowed, and therefore the last point is really more of a lint, since it can be so easily worked around.
Nomicon
A discussion of unsafe Rust would not be complete without pointing to the Nomicon.
Essentially, the rules of unsafe Rust are rather simple: uphold whatever guarantee the compiler would have if it was safe Rust.
This is not as helpful as it could be, since those rules are not set in stone yet; sorry.
Then, what are the semantics for dereferencing raw pointers?
As far as I know1:
&T
or &mut T
) then you must ensure that the aliasing rules these references obey are upheld,That is, providing that the caller had mutable access to the location:
pub unsafe fn run_ptr_direct(a: *const i32, b: *mut f32) -> (i32, i32) {
let x = *a;
*b = 1.0;
let y = *a;
(x, y)
}
should be valid, because *a
has type i32
, so there is no overlap of lifetime in references.
However, I would expect:
pub unsafe fn run_ptr_modified(a: *const i32, b: *mut f32) -> (i32, i32) {
let x = &*a;
*b = 1.0;
let y = *a;
(*x, y)
}
To be undefined behavior, because x
would be live while *b
is used to modify its memory.
Note how subtle the change is. It's easy to break invariants in unsafe
code.
1 And I might be wrong right now, or I may become wrong in the future