Smart Pointers · about 35 min

Rc: Shared Ownership

Why this matters

Ownership in Rust normally means exactly one owner. Some data really is shared, though: a configuration used by many workers, a node in a graph with several incoming edges, a cached value referenced from several places. Borrowing does not fit when no single owner clearly outlives all the users. Rc<T> (reference counted) gives a value several owners: it stays alive as long as at least one of them does, and is freed automatically when the last one goes away.

Using Rc

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("shared text"));
    println!("count after new: {}", Rc::strong_count(&a));
    let b = Rc::clone(&a);
    println!("count after clone: {}", Rc::strong_count(&a));
    {
        let c = Rc::clone(&a);
        println!("inside block: {} ({} owners)", c, Rc::strong_count(&a));
    }
    println!("after block: {}", Rc::strong_count(&a));
    println!("same allocation: {}", Rc::ptr_eq(&a, &b));
    println!("length via deref: {}", b.len());
}
count after new: 1
count after clone: 2
inside block: shared text (3 owners)
after block: 2
same allocation: true
length via deref: 11

In memory

 a ----+
       |      heap allocation
 b ----+--->  +----------------------+
       |      | strong count: 3      |
 c ----+      | weak count: 0        |
              | value: "shared text" |  (a String: ptr, len, cap -> its own buffer)
              +----------------------+

The counts live next to the value in one allocation. Every clone and drop updates them, which costs a few instructions: cheap, but not free, unlike borrowing.

Rc gives shared, read-only access

Since several owners can reach the value at once, Rc<T> only gives out &T. Trying to mutate fails:

use std::rc::Rc;

fn main() {
    let shared = Rc::new(vec![1, 2, 3]);
    let other = Rc::clone(&shared);
    shared.push(4);
    println!("{:?}", other);
}
error[E0596]: cannot borrow data in an `Rc` as mutable
 --> main.rs:6:5
  |
6 |     shared.push(4);
  |     ^^^^^^ cannot borrow as mutable
  |
  = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<Vec<i32>>`

This is the aliasing rule from module 5 again: many owners means shared access, so no mutation. When shared data must change, combine Rc with RefCell (next lesson).

Sharing structure

A classic use: data structures where parts are shared. Two lists with a common tail:

use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let common = Rc::new(Cons(10, Rc::new(Cons(20, Rc::new(Nil)))));
    let a = Cons(1, Rc::clone(&common));
    let b = Cons(2, Rc::clone(&common));
    println!("a = {:?}", a);
    println!("b = {:?}", b);
    println!("common has {} owners", Rc::strong_count(&common));
}
a = Cons(1, Cons(10, Cons(20, Nil)))
b = Cons(2, Cons(10, Cons(20, Nil)))
common has 3 owners
 a: Cons(1, ---+
               +--> Cons(10, --> Cons(20, --> Nil
 b: Cons(2, ---+       (strong count 3: common, a, b)

With Box, the tail would have to be copied for the second list, since a Box has exactly one owner.

Rc is single-threaded

The count is updated with ordinary (non-atomic) instructions, which is fast but unsafe if two threads updated it at once. So Rc cannot be sent to another thread; the compiler enforces it, as you will see in module 14. The thread-safe version is Arc (atomic reference counting), with the same API.

When to use Rc

Prefer plain borrowing when one owner clearly outlives the users: &T is free and checked at compile time. Prefer Box when there is one owner. And beware of cycles: two Rcs pointing at each other keep each other alive forever (lesson 4).

Common mistakes

Recap

Going further: read the Rust Book chapter 15.4. Try Rc::try_unwrap(rc), which gives you the value back only if you hold the last reference.

Exercises

Exercise 1 · write code

Share one value between owners

Several Workers must share one Config without copying it. Define

struct Config { name: String, retries: u32 }
struct Worker { id: u32, config: Rc<Config> }

The given main reads a config name from stdin, creates the config once in an Rc, creates three workers that each hold an Rc::clone of it, and prints the reference count before and after dropping one worker:

worker 1 uses prod (3 retries)
worker 2 uses prod (3 retries)
worker 3 uses prod (3 retries)
owners: 4
owners after drop: 3

Implement fn describe(&self) -> String on Worker.

File: main.rs

Starting point

use std::io;
use std::rc::Rc;

fn main() {
    let mut name = String::new();
    io::stdin().read_line(&mut name).unwrap();
    let config = Rc::new(Config { name: name.trim().to_string(), retries: 3 });
    let mut workers = Vec::new();
    for id in 1..=3 {
        workers.push(Worker { id, config: Rc::clone(&config) });
    }
    for w in &workers {
        println!("{}", w.describe());
    }
    println!("owners: {}", Rc::strong_count(&config));
    workers.pop();
    println!("owners after drop: {}", Rc::strong_count(&config));
}

Example

input

prod

expected output

worker 1 uses prod (3 retries)
worker 2 uses prod (3 retries)
worker 3 uses prod (3 retries)
owners: 4
owners after drop: 3
Hint 1

Fields of the shared value are reached through the Rc by auto-deref: self.config.name.

Reference solution
use std::io;
use std::rc::Rc;

struct Config {
    name: String,
    retries: u32,
}

struct Worker {
    id: u32,
    config: Rc<Config>,
}

impl Worker {
    fn describe(&self) -> String {
        format!("worker {} uses {} ({} retries)", self.id, self.config.name, self.config.retries)
    }
}

fn main() {
    let mut name = String::new();
    io::stdin().read_line(&mut name).unwrap();
    let config = Rc::new(Config { name: name.trim().to_string(), retries: 3 });
    let mut workers = Vec::new();
    for id in 1..=3 {
        workers.push(Worker { id, config: Rc::clone(&config) });
    }
    for w in &workers {
        println!("{}", w.describe());
    }
    println!("owners: {}", Rc::strong_count(&config));
    workers.pop();
    println!("owners after drop: {}", Rc::strong_count(&config));
}

Exercise 2 · write code

Lists sharing a tail

With enum List { Cons(i32, Rc<List>), Nil }, two lists can share a common tail. Write fn sum(list: &List) -> i32. The given main builds tail = 5 -> 10, then a = 3 -> tail and b = 4 -> tail, and prints:

a sum 18, b sum 19
tail owners: 3

File: main.rs

Starting point

use std::rc::Rc;

fn main() {
    let tail = Rc::new(List::Cons(5, Rc::new(List::Cons(10, Rc::new(List::Nil)))));
    let a = List::Cons(3, Rc::clone(&tail));
    let b = List::Cons(4, Rc::clone(&tail));
    println!("a sum {}, b sum {}", sum(&a), sum(&b));
    println!("tail owners: {}", Rc::strong_count(&tail));
}

Example

expected output

a sum 18, b sum 19
tail owners: 3
Hint 1

Same recursion as with Box: List::Cons(v, rest) => v + sum(rest); &Rc<List> derefs to &List.

Reference solution
use std::rc::Rc;

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

fn sum(list: &List) -> i32 {
    match list {
        List::Cons(v, rest) => v + sum(rest),
        List::Nil => 0,
    }
}

fn main() {
    let tail = Rc::new(List::Cons(5, Rc::new(List::Cons(10, Rc::new(List::Nil)))));
    let a = List::Cons(3, Rc::clone(&tail));
    let b = List::Cons(4, Rc::clone(&tail));
    println!("a sum {}, b sum {}", sum(&a), sum(&b));
    println!("tail owners: {}", Rc::strong_count(&tail));
}

Exercise 3 · quiz

What does Rc::clone do?

let a = Rc::new(String::from("big text")); let b = Rc::clone(&a); Which statements are true? Select all that apply.

Answer
  • The reference count goes from 1 to 2; a and b point at the same heap allocation

  • The String is freed only when both a and b have been dropped

Rc::clone copies a pointer and increments the count: no data is copied, which is why the explicit Rc::clone(&a) style is preferred over a.clone() (it signals "cheap"). The value is dropped when the count reaches zero. Rc only hands out shared references, so neither owner can mutate the String; that needs RefCell (next lesson).