Ownership and Lifetimes
Storage Locations
Registers: (and pseudo-registers) Contain 64-bit values that are being used in the current computation. Are nowhere in memory (notionally). Not really visible in Rust
Global Storage: Allocated and initialized at program startup. Rust doesn't use this much. The
static
keyword denotes global storageThe Stack: Each time a function is called, a "stack frame" is allocated. Things that are temporary to that function are stored there. When the function returns, it is all lost.
let
variables and function parameters are stack-allocatedThe Heap: Area of dynamically allocated memory. OS and language runtime cooperate to allocate enough to run and to reclaim unused storage. Rust's
Box<T>
type has its contents heap-allocated: other Rust types such asString
andVec
are built out ofBox
es.
Standard Memory Management
Programming without dynamic memory allocation and deallocation?
Two standard dynamic memory models
Automatic: Garbage collector or reference counting system
Garbage Collector: When low on memory, trace out all accessible memory, free non-accessible
Reference Counting: Keep track of how many references to a particular chunk of memory. When count goes to zero, free it
Manual: Programmer keeps track of which memory should be preserved, allocates new memory, frees old
Rust Memory Management
Invisible Manual: Compiler issues code to allocate memory and free memory where needed
This is restrictive: programmer must ensure that memory is not allocated too late or freed to early, in the presence of pointers
Rust compiler ensures that memory is allocated before use, statically unavailable at time of free
Key is lexical scope: when a variable statically leaves scope, its value is no longer reachable, so freed
Memory Allocation
By default, a value must be allocated in memory unless it is small enough to fit in a register and marked copyable and bleah bleah bleah
Choices are stack allocation or heap allocation: default is stack
Heap allocation is ultimately done in unsafe code
Drop
trait allows explicit actions during deallocation
Copyable Values
If a type has the
Copy
trait (e.g. the integer types) the compiler will feel free to make a (deep) copy of it whenever convenientIf a type has the
Clone
trait (e.g. most built-in types) the compiler will make a (deep) copy whenever the type'sclone()
method is calledOtherwise there will be no user-visible copying
Moves
The compiler may choose to insert code to (notionally) move a thing to a different place in memory
If this happens, it will not make a copy: it will leave the old thing uninitialized and unreferenceable, and then reclaim its storage
Ownership
Net effect of all this: at any given time a value is "owned" by a particular name
The value is given to the owning name when it is created (Resource Acquisition Is Initialization = RAII)
The value is freed when the owning name leaves scope
Ownership can be transferred by a move
Mutability
By default, an owned thing is expected to be left alone
To change its insides, the owned thing must be marked
mut
. This is essentially just a compiler sanity checkA
mut
thing is deep-mutable: any of its owned insides can be changed
Lifetimes
Rust tracks ownership via explicit or implicit lifetime specifiers
Format is
'a
, read "tick a"Compiler's "borrow checker" tracks lifetime and ownership of values, throws a static error when can't work
Usually implicit, but can be made explicit when needed / wanted
struct S<'a> { field: T<'a>, }
The Takeaway
Need to develop an operational mental model of ownership and lifetimes
When confused, refer to that model or get help
Refs, Ownership, Lifetimes
Can take a reference to an owned thing
let y = 5; let ry = &y;
Now you have an obligation:
ry
must not outlivey
C/C++ will happily let you write things like
int y = 5; int *ry = &y; return ry;
even though the returned pointer is now pointing into whereever on the stack the now-lost
y
wasRust compiler tracks this for you, so that e.g.
let y = 5; let ry = &y; return ry;
will give a compile-time error
Mutable Refs
Refs come in two varieties: "mutable" and "immutable" (really "exclusive" and "shared")
Can only take mutable ref to mutable thing
let mut y = 5; let ry = &mut y;
Once you take a mutable ref, you've essentially "borrowed" the value referred to
- Must not go out of scope
- Cannot take any more references while it is live
- Owner can't do anything with the value while it is live (read it, change it, move it, drop it)
Immutable Refs
An immutable ref is essentially "shared". You can take lots of them if you want
let y = 5; let ry1 = &y; let ry2 = &y;
You are still restricted for safety
- Owner cannot drop or move value (nor mutate, duh) while refs are live
More About Refs
A lot of automatic derefing happens
- With the "." structure / enum operator
- With arithmetic operators
- etc
- playground
There's no such thing as a "null reference" (in safe code): no way to produce one, no need to guard against them
If you need a "nullable" value, use the
Option
typelet y = 10; let ory = Some(&y);
This is true for refs or anything else
You can get a reference to an anonymous variable implicitly defined by an expression
let ry = &10;
Explicit Lifetimes
The machinery that the compiler uses to check lifetimes is by default "under the hood": does the checking for you without intervention
Sometimes, though, you need (or want) to get explicit access to that machinery to allow a program to compile that is safe but won't by default
"Named lifetimes" start with a tick, e.g.
'a
("tick-a")In many contexts, explicit lifetime names can be declared
fn f<'a, 'b>(x: &u64, y: &u64) -> &u64
These names can then be used to describe lifetime constraints for referenced data
fn f<'a, 'b>(x: &'a u64, y: &'b u64) -> &'a u64
In this example, the returned data must not be used after
x
diesfn f<'a, 'b: 'a>(x: &'a u64, y: &'b u64) -> &'a u64
We can also require that result data not be used after
y
diesfn f<'a, 'b: 'a>(x: &'a u64, y: &'b u64) -> &'b u64
https://play.rust-lang.org/?gist=59636f6153699652df21d05ea61f3428&version=stable
Book Example
Rust Parametric Types
Actually "monomorphic" or "template" types, but…
Just like we can provide lifetime variables, can provide type variables to get "generic" thingies
fn id<T>(x: T) -> T { x }
Can now call with whatever type as long as they match
assert_eq!(id(5u32), 5u32); assert_eq!(id("hello"), "hello");
Much more to say on this topic later in the quarter