|
Rusts references behave like plain raw C/C++ pointers at runtime, without any bookkeeping code running at all. The magic all lies in the compiletime borrow checker, which roughly works like this: - All data is accessed either through something on the stack or in static memory.
- Accessing data, say by creating a reference to it,
causes the compiler to "borrow" the value for the scope in which the reference
is alive.
- The references can be alive for any scope equal or smaller than for which
access to the data itself is valid.
- References track the original scope for which they are alive around as a
template-paramter-like thing called "lifetime parameter".
Note that Rusts use of the word "lifetime" is thus a bit narrower than the
one used in C++, since it just talks about stack scopes, and not the lifetime
of the actual value as would be tracked by a GC or ref counting.
Example:
let x = true;
let r = &x;
Here, r would infer to a type like `Reference<ScopeOfXVariable, bool>`.
(The actual type in rust would be a `&'a T` with
'a = scope of x, and T = bool).
- Because the scope is tracked as part of the reference type,
it is possible to copy/move/transform/wrap references safely, since
the compiler will always "know" about the original scope and thus can
check that you never end up in a situation where you accidentally outlive the
thing you borrowed, say if you try to return a type that contains a reference
somewhere deep down.
- The borrow itself acts as a compiletime read/write lock on the thing you referenced,
so for the scope that the reference is alive for the compiler prevents
you from changing or destroying the referenced thing. Example:
// This errors:
let mut a = 5;
let b = &a;
a = 10; // ERROR: a is borrowed
println!("{}", *b);
// This is fine:
let mut c = 100;
{
let d = &c;
println!("{}", *d);
}
c = 50;
- The above examples just use `&` for references, but Rust has two references types:
- &'a T, called "shared reference", which cause "shared borrows".
- &'a mut T, called "mutable references", which cause "mutable borrows".
- Both behave the same in principle, but have different restrictions and guarantees:
- A mutable borrow is exclusive, meaning no other other borrow to the same data
is allowed while the &mut T is alive, but allows you to freely change the T through
the reference.
- A shared borrow may alias, so you can have multiple &T pointing
to the same data at the same time, but you are not allowed to freely change T through
the reference.
- (If those two cases are too rigid there is also a escape hatch that
a specific type may opt-into to allow mutation of itself through a shared reference, with
exclusivity checked through some other mechanism like runtime borrow counting.)
- Through these two reference types, Rust libraries can abstract with arbitrary APIs
without loosing the borrow checker guarantees. Eg, the "reference to vector element"
example boils down as this:
let mut v = Vec::new();
v.push(1);
let r = &v[0]; // the reference in r now has a shared borrow on v.
v.push(2); // push tries to create a mutable borrow of v, which conflicts with the
borrow kept alive by r, so you get a borrow error at compiletime.
println!("{}", *r);
The important part is that all this is there, per default, for all Rust code in existence, so you can not accidentally ignore it like a library solution you might not know about, or like language features that don't know about the library solutions. |