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:

  1. assignment
  2. 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., &elements or &mut elements) do not move owners.

Ending Ownership

When an owner’s scope ends, any resources it owns are deleted.

Note

for loops are a confusing case:

for element in elements {
  // ...
}

Ownership transfers into the loop, meaning elements is deleted after the loop ends.

Same as a function, if you instead provide a reference to the elements it 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 i32 compile and not our type which just wraps it?

Answer

Primitive types in Rust have special behavior. These implement the Copy trait.
Types implementing Copy are 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.

ownership_stats

Resolving Ownership Issues

4 strategies for ownership issues:

  1. Use references where full ownership is not required.
  2. Refactor code to reduce the number of long-lived objects.
  3. Duplicate the value.
  4. 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:

  • MOVE C, Go Instead of passing ownership default to making a copy.
  • READ-REF Go doesn’t have this, always MUT-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)
CostMay be slow/expensiveAlways fast/cheap
InvocationExplicit .clone() requiredImplicit (on assignment and return)
ResultMay differ author-definedBit-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) Copy for me”.
It literally just injects the impl in 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.