Ownership
Last edited
Ownership
Question
Why does ownership exist?
Answer
Rust doesn’t have a garbage collector. The garbage collector’s job is to check if it’s safe to delete things. Garbage collector has run-time costs, resulting in less predictable performance and latency.
The solution to no GC is ownership. Kill things the first chance you implicitly can.
Rules of ownership
- only one owner
- when the owner leaves the scope, it must be safe to delete the owned data.
Create an owner
Create a value and bind it to a variable (also called assignment).
// elements is the owner of the vec data.
let elements = Vec::new();Transferring Ownership
Transferring ownership is called moving. If we don’t want to move mv we can do the opposite and copy cp, making a new copy of the data with a new owner.
How to transfer ownership
There are 2 ways:
- assignment
- passing data through a function barrier
fn main() {
// --- Assignment ---
// In most languages you'd be able to use both variables, but not in Rust
let sat_a = CubeSat { id: 0 };
let new_sat_a = sat_a; // sat_a no longer in main
// --- Function barrier ---
let sat_a = CubeSat { id: 0 };
let a_status = check_status(sat_a); // sat_a no longer in main, ownership has been transferred to check_status.
}Note
functions that take references as arguments (e.g.,
&elementsor&mut elements) do not move owners.
Ending Ownership
When an owner’s scope ends, any resources it owns are deleted.
Note
forloops are a confusing case:for element in elements { // ... }Ownership transfers into the loop, meaning
elementsis deleted after the loop ends.Same as a function, if you instead provide a reference to the
elementsit will not transfer ownership// These two do NOT move ownership (same as functions) for element in &elements {} // or for element in &mut elements {}
The problem
fn check_status(sat_id: CubeSat) -> StatusMessage {
StatusMessage::Ok
}
// Works
let sat_a = 0;
let a_status = check_status(sat_a);
let a_status = check_status(sat_a);
// Ownership issue
struct CubeSat {
id: u64
}
let sat_a = CubeSat{id: 0}; // Ownership starts here, sat_a owns the data
let a_status = check_status(sat_a); // Ownership moves to check_status(), but is not returned
let a_status = check_status(sat_a); // sat_a is no longer the owner of the object, access is invalid.
Question
Why does the primitive type of
i32compile and not our type which just wraps it?
Answer
Primitive types in Rust have special behavior. These implement the
Copytrait.
Types implementingCopyare duplicated at times that would otherwise be illegal. This can be for convenience but can confuse noobs into thinking they’re allowed to do this.
Formally, primitive types are said to possess copy semantics, whereas all other types have move semantics.
Solutions
Returning ownership
We’ll discuss other options in the following sections.
One solution could be returning ownership back to the original variables:
fn check_status(sat_id: CubeSat) -> CubeSat {
// Print the status rather than returning it
println!("{:?}: {:?}", sat_id, StatusMessage::Ok);
sat_id // return ownership
}
fn main () {
let sat_a = CubeSat { id: 0 };
let sat_a = check_status(sat_a);
let sat_a = check_status(sat_a);
}I’ve skipped sat_b, sat_c for terseness but the image includes them.

Resolving Ownership Issues
4 strategies for ownership issues:
- Use references where full ownership is not required.
- Refactor code to reduce the number of long-lived objects.
- Duplicate the value.
- Wrap your data in a type designed to assist with movement issues.
1 references where full ownership is not required
Here’s a few signatures.
C and Go have the same signature functionality, with the exceptions:
MOVEC, GoInstead of passing ownership default to making a copy.READ-REFGodoesn’t have this, alwaysMUT-REF.
// MOVE: Give me ownership.
// I'll do what I want and maybe I'll give an updated CubeSat back.
// If I don't, CubeSat dies with me.
fn send(to: CubeSat) // -> CubeSat (if we did give it back)
// READ-REF: Give me a read-only reference.
// I won't change your CubeSat, I'll just read from it.
// CubeSat will survive.
fn send(to: &CubeSat)
// MUT-REF: Give me a mutable reference.
// I can change the actual CubeSat you referred me to.
// CubeSat will survive.
fn send(to: &mut CubeSat)2 Use fewer long-lived values
There’s not a ton to gleam from this example, the point is he avoided dragging around CubeSat for the lifetime of main by just using a vec of id.
Tip
IMO this REALLY depends on your task, I find this much less readable and extendable. Perhaps just something to think about and use if it’s easier.
fn fetch_sat_ids() -> Vec<u64> {
vec![1, 2, 3]
}
fn main() {
let mut mail = Mailbox { messages: vec![] };
let base = GroundStation {};
// send loop: no sat, just drop a message keyed by id into the central mail
for sat_id in fetch_sat_ids() {
base.send(
&mut mail,
Message { to: sat_id, content: String::from("hello") });
}
// recv loop: make a throwaway sat, pull its message back out by id
for sat_id in fetch_sat_ids() {
let sat = base.connect(sat_id);
let msg = sat.recv(&mut mail);
println!("{:?}: {:?}", sat.id, msg);
}
}3 Duplicate the value
Simply copy the value.
Doing so is often frowned upon but it can be useful.
Primitive types like integers are a good example.
Primitive types are cheap for a CPU to duplicate - so cheap that Rust always copies these if it would otherwise worry about ownership being moved.
Types can opt into two modes of duplication:
- cloning
- copying
Copy acts implicitly:
Whenever ownership would otherwise be moved to an inner scope, the value is duplicated instead. (The bits of object a are replicated to create object b.)
Clone acts explicitly:
Types that implement Clone have a .clone() method that is permitted to do whatever it needs to do to create a new value.
Clone being slower, emphasis on the may, Copy guarantees it’s just copying bits, while clone can, if the creator decides, can do more logic.
So a clone implementation that literally just copies bits, will be the exact same speed as copy - just not implicit (you need to call the method).
Clone (std::clone::Clone) | Copy (std::marker::Copy) | |
|---|---|---|
| Cost | May be slow/expensive | Always fast/cheap |
| Invocation | Explicit .clone() required | Implicit (on assignment and return) |
| Result | May differ author-defined | Bit-for-bit identical |
Implementing Copy
Using our earlier, initially broken example that would give up ownership.
Deriving the Copy implementation
Note
The
#[derive(X)]is a macro which generates code for you at compile time. It’s not inheriting anything. We’re saying “read my type and generate some code that implements (impl)Copyfor me”.
It literally just injects theimplin the code.
#[derive(Copy,Clone,Debug)]
struct CubeSat {
id: u64,
}
#[derive(Copy,Clone,Debug)]
enum StatusMessage {
Ok,
}
fn check_status(sat_id: CubeSat) -> StatusMessage {
StatusMessage::Ok
}
fn main() {
let sat_a = CubeSat { id: 0 };
let a_status = check_status(sat_a); // Doesn't give up ownership of sat_a (implicitly calls Copy)
let a_status = check_status(sat_a); // This now works
}Implementing Copy ourselves
Here’s how we implemented the Copy trait on our types.
Implementing Copy requires an implementation of Clone
There are two ways we implement Clone, both are shown.
impl Copy for CubeSat { }
impl Copy for StatusMessage { }
// 2) Write out the creation of the new object
impl Clone for CubeSat {
fn clone(&self) -> Self {
CubeSat { id: self.id }
}
}
// 1) Simply dereference self
impl Clone for StatusMessage {
fn clone(&self) -> Self {
*self
}
}
// Calling functions we can now use clone or copy
fn main() {
let sat_a = CubeSat { id: 0 };
let a_status = check_status(sat_a.clone()); // explicitly (CLONE)
let a_status = check_status(sat_a); // implicitly (COPY)
}4 Wrap data within specialty types
Basically it’s opting into garbage collection for a single variable. It lets you move it around and not worry about ownership as the reference counter will keep track of if it’s needed or not.
std::rc::Rc is a wrapper to make a reference counter.
Reference counting is used to track valid references.
- As each reference is created, an internal counter increases by one.
- When a reference is dropped, the count decreases by one.
- When the count hits zero, T is also dropped.
Minimal example:
use std::rc::Rc;
#[derive(Debug)]
struct GroundStation {}
fn main() {
let base = Rc::new(GroundStation {});
println!("{:?}", base);
}Rc<T> is not thread safe.
Rc<T> does not allow mutation. To permit that you need to wrap the wrapper using Rc<RefCell<T>> then you can mutate the data you are reference counting.