Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 7: Rc - Reference Counting

The Real Problem: Multiple Owners Need the Same Data

Rust's ownership model says every value has exactly one owner. This prevents many bugs, but sometimes multiple parts of your code legitimately need to own the same data:

#![allow(unused)]
fn main() {
struct Config {
    db_url: String,
    api_key: String,
}

// This won't work - who owns the config?
let config = Config::load();
let server = Server::new(config);    // server takes ownership
let logger = Logger::new(config);    // ❌ ERROR: config already moved!
}

You can't:

  • Clone Config each time (expensive, inconsistent if mutated)
  • Use references (lifetime gets complicated with multiple owners)
  • Use Box (still single ownership)

You need: Multiple owners to share the same data.

What Rc Actually Does

Rc<T> (Reference Counted) enables shared ownership by tracking how many owners exist:

#![allow(unused)]
fn main() {
use std::rc::Rc;

let config = Rc::new(Config::load());
let server = Server::new(Rc::clone(&config));  // ✅ +1 owner
let logger = Logger::new(Rc::clone(&config));  // ✅ +1 owner
// config, server, and logger all share ownership of the same Config
}

How it works:

  1. Allocates the value on the heap (like Box)
  2. Keeps a reference count alongside the value
  3. Each Rc::clone() increments the count (and creates a new pointer)
  4. Each drop decrements the count
  5. When count reaches 0, the value is freed

Why Rc? Comparing the Three Approaches

The Key Insight: Rc = Multiple Pointers to THE SAME Heap Data

With Rc<T> (What we want):

RcptrRcptrRcptrSTACKConfigdburl:Stringapikey:StringALLthreeRcpointerspointtoTHISSAMEallocation!HEAP

Multiple owners, one allocation, shared data

  • All three Rc point to the exact same Config in memory
  • Memory is freed only when the last owner drops
  • For mutation, see Chapter 13 (Rc + RefCell)

Why Not Box<T>?

With Box<T> (Doesn't work for sharing):

ConfigcopyConfigcopyConfigcopySTACKBoxptrBoxptrBoxptrHEAPSeparateallocationAnotherseparateallocationYetanotherallocation

Problem: Each Box owns a DIFFERENT copy

  • You'd have to clone the Config for each component (expensive!)
  • Each copy is independent - changes to one don't affect others
  • Three separate heap allocations wasting memory
  • Box::new(config) moves config, making it unavailable for the next component
#![allow(unused)]
fn main() {
let config = Config::load();
let server = Box::new(config);  // config moved here
let logger = Box::new(config);  // ❌ ERROR: config already moved!
}

Why Not References &T?

With references (Lifetime hell):

ConfigStackHeap&config&config&configAllthereferencessamedatapointtoButWHOownsit?(or Stack)(good!)

Problem: References don't own - lifetimes get complex

First, let's define structs that use references:

#![allow(unused)]
fn main() {
struct Server<'a> { config: &'a Config }
struct Logger<'a> { config: &'a Config }
}

Problem 1: Can't return from functions

When you try to return a struct containing a reference to local data, it fails:

#![allow(unused)]
fn main() {
fn create_server() -> Server {
    let config = Config::load();
    let server = Server { config: &config };
    server  // ❌ ERROR: cannot return value referencing local variable `config`
}
}

Why it fails: config is dropped at the end of the function, but server.config still references it!

Problem 2: Lifetimes infect everything like a virus

You can store references locally - that works fine:

#![allow(unused)]
fn main() {
let config = Config::load();
let server = Server { config: &config };
let logger = Logger { config: &config };

// Storing in a Vec works locally
let mut components: Vec<Server<'_>> = Vec::new();
components.push(server);  // ✅ This works here!
}

But now the lifetime constraint INFECTS everything that touches it:

Want to return the Vec? Now the function needs lifetimes:

#![allow(unused)]
fn main() {
fn make_components<'a>(cfg: &'a Config) -> Vec<Server<'a>> {
    vec![Server { config: cfg }]
}
}

Want to store in a struct? Now the struct needs lifetimes:

#![allow(unused)]
fn main() {
struct App<'a> {
    components: Vec<Server<'a>>,
}
}

Want to store App in another struct? That needs lifetimes too:

#![allow(unused)]
fn main() {
struct System<'a> {
    app: App<'a>,
}
}

Every function that uses System needs lifetimes:

#![allow(unused)]
fn main() {
fn process_system<'a>(sys: &System<'a>) {
    // Process the system
}
}

This cascades through your ENTIRE codebase - all because Server contains a reference!

How Rc Solves These Problems

Now let's see how Rc fixes both issues:

Solution to Problem 1: Returning from functions works

With Rc, you can return structs containing shared data - no lifetime issues:

#![allow(unused)]
fn main() {
use std::rc::Rc;

struct Server { config: Rc<Config> }  // No lifetime parameter!

fn create_server() -> Server {
    let config = Rc::new(Config::load());
    Server { config }  // ✅ Works! Rc owns the data
}
}

Why it works: Rc owns the data (keeps it alive), unlike references which just borrow. When you return Server, you're moving ownership of the Rc out of the function.

Solution to Problem 2: No lifetime infection

With Rc, no lifetime parameters needed anywhere:

#![allow(unused)]
fn main() {
use std::rc::Rc;

// No lifetime parameters!
struct Server { config: Rc<Config> }
struct Logger { config: Rc<Config> }

// Storing in a Vec - no lifetime needed
let config = Rc::new(Config::load());
let server = Server { config: Rc::clone(&config) };
let logger = Logger { config: Rc::clone(&config) };

let mut components = Vec::new();
components.push(server);  // ✅ Works!

// Functions don't need lifetime parameters
fn make_components(cfg: Rc<Config>) -> Vec<Server> {
    vec![Server { config: cfg }]
}

// Structs don't need lifetime parameters
struct App {
    components: Vec<Server>,
}

struct System {
    app: App,
}

// Functions using System don't need lifetime parameters
fn process_system(sys: &System) {
    // Process the system
}
}

Why it works: Rc provides ownership, not just borrowing. Lifetime tracking is moved from compile-time to runtime - instead of the compiler tracking lifetimes with 'a annotations, Rc tracks them at runtime with reference counts.

The key trade-off:

  • References (&T): Compile-time lifetime tracking → zero runtime overhead, but inflexible
  • Rc: Runtime reference counting → flexible ownership, but pays cost of counter increments/decrements

The lifetime problem:

  • If Server and Logger store &Config, they must live shorter than config
  • You can't return them from functions (references stack-local data)
  • You can't store them in collections easily (lifetime annotations everywhere)
  • Complex ownership patterns become impossible to express

When references work well:

  • Temporary borrows (function parameters)
  • Short-lived access patterns
  • When there's a clear owner and the borrow is brief

When you need Rc instead:

  • No clear single owner
  • Multiple components need to outlive each other independently
  • Dynamic lifetimes (can't determine at compile time)
  • Building complex data structures (graphs, trees with shared nodes)

Summary: Why Rc Wins

ApproachShares Data?Multiple Owners?Lifetime Issues?
Rc<T>✅ Yes✅ Yes✅ No - runtime counted
Box<T>❌ No (copies)❌ No (single owner)✅ No
&T✅ Yes❌ No (borrowed)❌ Yes - compile-time tracked

Rc<T> gives you the best of both worlds:

  • Shared data like references (all point to same allocation)
  • Multiple ownership like separate boxes (but without the copies)
  • Simple lifetimes (no 'a annotations needed)
  • Runtime reference counting handles cleanup automatically

Using Rc: Basic Examples

Example 1: Shared Configuration

#![allow(unused)]
fn main() {
use std::rc::Rc;

let config = Rc::new(Config {
    db_url: String::from("localhost:5432"),
    api_key: String::from("secret"),
});

println!("Strong count: {}", Rc::strong_count(&config)); // 1

let server_config = Rc::clone(&config);  // Now 2 owners
let logger_config = Rc::clone(&config);  // Now 3 owners

println!("Strong count: {}", Rc::strong_count(&config)); // 3

// All three can access the same data
println!("Server sees: {}", server_config.db_url);
println!("Logger sees: {}", logger_config.db_url);
}

Example 2: Deref Coercion Works

Because Rc implements Deref, you can use it like a reference:

#![allow(unused)]
fn main() {
let name = Rc::new(String::from("Alice"));

// Deref coercion: Rc<String> -> &String -> &str
fn print_name(s: &str) {
    println!("Name: {}", s);
}

print_name(&name);  // ✅ Works! &Rc<String> coerces to &str
println!("Length: {}", name.len());  // ✅ Call String methods directly
}

Example 3: Automatic Cleanup via RAII

RAII (Resource Acquisition Is Initialization): When a variable goes out of scope, its Drop is called automatically.

#![allow(unused)]
fn main() {
{
    let data = Rc::new(vec![1, 2, 3]);
    println!("Count: {}", Rc::strong_count(&data));  // 1

    {
        let shared = Rc::clone(&data);
        println!("Count: {}", Rc::strong_count(&data));  // 2
    } // `shared` dropped here, count decrements to 1

    println!("Count: {}", Rc::strong_count(&data));  // 1
} // `data` dropped here, count goes to 0, memory freed
}

Why this matters: You don't need to manually manage the reference count. Rust's ownership system handles it automatically through Drop.

Why Clone is Cheap

The Confusion: Two Different "Clones"

#![allow(unused)]
fn main() {
let data = vec![1, 2, 3];
let clone1 = data.clone();  // ❌ EXPENSIVE: Copies all elements

let rc_data = Rc::new(vec![1, 2, 3]);
let clone2 = rc_data.clone();  // ✅ CHEAP: Just increments a counter
}

Visual comparison:

Cloning a Vec directly - EXPENSIVE (O(n)):

BeforecloneAftercloneStackHeapdata123StackHeapdata123clone1123NEWcopy!Twoseparateallocations,allelementscopied

Cloning an Rc<Vec> - CHEAP (O(1)):

1123count:2123count:BeforeAfterRcStackHeapStackHeaprcdatarcdataclone2Sameallocation,counterincremented:::clone:

Rc::clone() only clones the pointer, not the data!

  • Cloning the Rc = increment counter + copy pointer (O(1))
  • Cloning the inner data = depends on the data (could be O(n))

Convention: Use Rc::clone(&rc) for Clarity

#![allow(unused)]
fn main() {
let rc = Rc::new(String::from("hello"));

// Both work, but one is clearer:
let clone1 = rc.clone();           // Looks like it might be expensive
let clone2 = Rc::clone(&rc);       // ✅ Clearly cloning the Rc, not the String
}

The explicit Rc::clone(&rc) syntax makes it obvious you're doing a cheap pointer clone, not an expensive data clone.

Rc Only Gives You Shared References

Important limitation: Rc<T> only provides &T, never &mut T.

#![allow(unused)]
fn main() {
let data = Rc::new(vec![1, 2, 3]);
let borrowed: &Vec<i32> = &*data;  // ✅ Can get &T
// let mut_borrowed: &mut Vec<i32> = &mut *data;  // ❌ ERROR: cannot borrow as mutable
}

Why? If multiple owners could get &mut T, you'd have multiple mutable references to the same data - a data race!

Solution for mutation: Use Rc<RefCell<T>> (covered in Chapter 13) for interior mutability.

Don't Confuse get_mut with Rc's Purpose

Like Cell and RefCell (see Chapters 5-6), Rc has a get_mut() method - but it's not the main point:

#![allow(unused)]
fn main() {
let mut rc = Rc::new(5);
if let Some(data) = Rc::get_mut(&mut rc) {
    *data += 1;  // Requires &mut Rc<T> AND strong_count == 1
}
}

The key distinction (same as Cell/RefCell):

  • get_mut(): Requires &mut self → compile-time checked → defeats the purpose
  • Rc::clone(): Only needs &self → shared ownership → this is the point!

If you're the sole owner (strong_count == 1), you don't need Rc at all! The whole point of Rc is enabling multiple owners.

Common Patterns

Pattern 1: Shared Data Across Components

The most common use case - multiple components need read access to the same data:

#![allow(unused)]
fn main() {
struct Server {
    config: Rc<Config>,
}

struct Logger {
    config: Rc<Config>,
}

struct Database {
    config: Rc<Config>,
}

let config = Rc::new(Config::load());
let server = Server { config: Rc::clone(&config) };
let logger = Logger { config: Rc::clone(&config) };
let db = Database { config: Rc::clone(&config) };

// All components can read the same config
println!("Server using: {}", server.config.db_url);
println!("Logger using: {}", logger.config.db_url);
}

Pattern 2: Tree Structures (Parent → Children)

Rc works well for tree structures where parents own children:

#![allow(unused)]
fn main() {
use std::rc::Rc;

struct Node {
    value: i32,
    children: Vec<Rc<Node>>,
}

let child1 = Rc::new(Node { value: 1, children: vec![] });
let child2 = Rc::new(Node { value: 2, children: vec![] });

let parent = Rc::new(Node {
    value: 0,
    children: vec![Rc::clone(&child1), Rc::clone(&child2)],
});

// child1 and child2 can be shared elsewhere too
let another_parent = Rc::new(Node {
    value: 10,
    children: vec![Rc::clone(&child1)],  // Shared child!
});
}

Pattern 3: Caching/Flyweight Pattern

Share immutable data to save memory:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::rc::Rc;

struct FontCache {
    fonts: HashMap<String, Rc<FontData>>,
}

impl FontCache {
    fn get(&mut self, name: &str) -> Rc<FontData> {
        self.fonts.entry(name.to_string())
            .or_insert_with(|| Rc::new(FontData::load(name)))
            .clone()  // Cheap Rc clone, not data clone
    }
}
}

Pattern 4: Functional Data Structures

Share structure between versions:

#![allow(unused)]
fn main() {
use std::rc::Rc;

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

let tail = Rc::new(List::Cons(2, Rc::new(List::Cons(3, Rc::new(List::Nil)))));

// Two lists sharing the same tail
let list1 = List::Cons(1, Rc::clone(&tail));
let list2 = List::Cons(0, Rc::clone(&tail));
// Both lists share the [2, 3] portion in memory
}

The Problem with Cycles: When Rc Leaks Memory

Rc can create memory leaks if you create reference cycles. Here's the conceptual problem:

Imagine two nodes referencing each other:

Rc<Node>2strong_count:next:2Rc<Node>strong_count:next:StackHeapab

When the stack variables drop:

  • Each node's count decrements by 1 (from stack variables)
  • But each node still has count = 1 (from the other node)
  • Neither reaches 0, so neither is freed
  • Memory leak! 💀

Why this happens: Each Rc in the cycle keeps the others alive. There's no "starting point" to begin deallocation.

The solution: Use Weak<T> references to break cycles. Weak is a non-owning reference that doesn't keep the value alive. This pattern is covered in Chapter 13 (Rc + RefCell), where you'll learn how to combine Rc, Weak, and interior mutability for practical data structures like graphs and trees with bidirectional references.

Building Our Own Rc

The Inner Structure

Before diving into implementation details, we need to answer a fundamental question: Why do we need RcInner? Why not just put the count in Rc itself?

Why We Need RcInner: The Shared Count Problem

Consider what happens if we try to store the count directly in each Rc:

#![allow(unused)]
fn main() {
// ❌ WRONG: Each Rc has its own count
struct Rc0<T> {
    ptr: *mut T,
    count: usize,  // Each Rc instance has its own count!
}
}

The problem: When you clone an Rc, you create a new struct with a separate count:

#![allow(unused)]
fn main() {
let rc1 = Rc::new(String::from("data"));  // rc1.count = 1
let rc2 = rc1.clone();                    // rc2.count = 1 (copied!)

// rc1 and rc2 have DIFFERENT counts!
// When rc1 drops, it sees count = 1, so it frees the memory
// When rc2 drops, it also sees count = 1, so it tries to free again
// 💀 Double free! Undefined behavior!
}

What we actually need: All Rc instances pointing to the same data must share the SAME count:

#![allow(unused)]
fn main() {
let rc1 = Rc::new(String::from("data"));
let rc2 = rc1.clone();
let rc3 = rc1.clone();

// All three need to see: count = 3
// When rc1 drops: count becomes 2
// When rc2 drops: count becomes 1
// When rc3 drops: count becomes 0 → NOW free the memory
}

The solution: Store the count with the data, not with the pointer

#![allow(unused)]
fn main() {
// ✅ CORRECT: Separate inner struct
struct RcInner<T> {
    strong_count: usize,  // Shared count, stored with the value
    value: T,
}

struct Rc0<T> {
    ptr: *mut RcInner<T>,  // Just a pointer to the shared data + count
}
}

Visual comparison:

Wrong approach (count in each Rc):

rc1ptr1count:data1rc2ptrcount:StackHeapEachRchasitsowncount!Theycancoordinate!WRONG't

Correct approach (count with data):

rc1ptr2datacount:rc2ptrStackHeapBothpointtotheSAMEcountCORRECTSharedcount!

In code:

#![allow(unused)]
fn main() {
// Each Rc is just a pointer
let rc1 = Rc::new(String::from("data"));
// Heap: RcInner { count: 1, value: "data" }

let rc2 = rc1.clone();
// rc2 gets a COPY of the pointer (not the count!)
// Both rc1.ptr and rc2.ptr point to the SAME RcInner
// RcInner.count is incremented: count = 2

drop(rc1);
// Follows rc1.ptr to the shared RcInner
// Decrements the shared count: count = 1
// Doesn't free (count != 0)

drop(rc2);
// Follows rc2.ptr to the SAME RcInner
// Decrements the shared count: count = 0
// NOW frees the memory ✅
}

Summary: RcInner exists to ensure the count lives with the data on the heap, not with the pointer on the stack. This way, all Rc instances pointing to the same data share the exact same count.


Now let's tackle the main challenge in implementing RcInner.

The Challenge: Mutating Counts Through Shared References

The problem: When you clone an Rc, you only have &self (shared reference), but you need to increment the count.

If we tried using a plain usize:

#![allow(unused)]
fn main() {
struct RcInner<T> {
    strong_count: usize,  // ❌ Can't mutate this from &self
    value: T,
}

impl<T> Clone for Rc0<T> {
    fn clone(&self) -> Rc0<T> {
        let inner = unsafe { &*self.ptr };
        inner.strong_count += 1;  // ❌ ERROR: cannot mutate through shared reference!
        Rc0 { ptr: self.ptr }
    }
}
}

This fails because:

  • clone() receives &self (the Clone trait requires this)
  • From &self, we get &RcInner<T> (shared reference to inner data)
  • Rust forbids mutation through shared references (prevents data races)
  • But we MUST increment the count!

The solution: Cell<usize>

Cell provides interior mutability for Copy types (covered in Chapter 5):

#![allow(unused)]
fn main() {
struct RcInner<T> {
    strong_count: Cell<usize>,  // ✅ Can mutate through &self!
    value: T,
}

impl<T> Clone for Rc0<T> {
    fn clone(&self) -> Rc0<T> {
        let inner = unsafe { &*self.ptr };
        // ✅ Works! Cell allows mutation through shared reference
        inner.strong_count.set(inner.strong_count.get() + 1);
        Rc0 { ptr: self.ptr }
    }
}
}

The Simplified Structure

For now, we'll keep our implementation simple:

#![allow(unused)]
fn main() {
use std::cell::Cell;

struct RcInner<T> {
    strong_count: Cell<usize>,  // Cell: mutate through &self
    value: T,                    // The actual data
}

struct Rc0<T> {
    ptr: *mut RcInner<T>,
}
}

Note: The actual implementation in src/rc.rs is more complete - it includes weak_count and uses ManuallyDrop<T> to support Weak references (non-owning pointers that don't keep the value alive). We'll cover Weak in Chapter 13 (Rc + RefCell).

new - Create with Count 1

#![allow(unused)]
fn main() {
impl<T> Rc0<T> {
    fn new(value: T) -> Rc0<T> {
        let inner = Box::new(RcInner {
            strong_count: Cell::new(1),
            value,
        });

        Rc0 {
            ptr: Box::into_raw(inner),
        }
    }
}
}

clone - Increment Count

#![allow(unused)]
fn main() {
impl<T> Clone for Rc0<T> {
    fn clone(&self) -> Rc0<T> {
        let inner = unsafe { &*self.ptr };
        inner.strong_count.set(inner.strong_count.get() + 1);
        Rc0 { ptr: self.ptr }
    }
}
}

Important: Rc::clone() is cheap! It just increments a counter. This is different from .clone() on the inner type which might be expensive.

Convention: Use Rc::clone(&rc) instead of rc.clone() to make it clear you're cloning the pointer, not the data.

Deref - Access the Value

#![allow(unused)]
fn main() {
impl<T> Deref for Rc0<T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { &(*self.ptr).value }
    }
}
}

Note: Rc only gives you &T (shared reference), never &mut T. This is intentional - if multiple owners could mutate, we'd have data races.

drop - Decrement and Maybe Free

#![allow(unused)]
fn main() {
impl<T> Drop for Rc0<T> {
    fn drop(&mut self) {
        let inner = unsafe { &*self.ptr };
        let count = inner.strong_count.get();
        inner.strong_count.set(count - 1);

        if count == 1 {
            // Last reference - deallocate everything
            unsafe {
                drop(Box::from_raw(self.ptr));
            }
        }
    }
}
}

When the last Rc drops (count becomes 0), we deallocate the entire RcInner, which automatically drops the value.

strong_count - Check Reference Count

#![allow(unused)]
fn main() {
impl<T> Rc0<T> {
    fn strong_count(this: &Rc0<T>) -> usize {
        unsafe { (*this.ptr).strong_count.get() }
    }
}
}

get_mut - Unique Access for Sole Owner

If you're the only owner (strong_count == 1), you can get &mut T:

#![allow(unused)]
fn main() {
impl<T> Rc0<T> {
    fn get_mut(this: &mut Rc0<T>) -> Option<&mut T> {
        if Rc0::strong_count(this) == 1 {
            // SAFETY: We're the sole owner, so no aliases exist
            unsafe { Some(&mut (*this.ptr).value) }
        } else {
            None
        }
    }
}
}

The Complete Implementation

Note: The actual implementation in src/rc.rs is more complete - it includes full Weak0<T> support and uses ManuallyDrop<T> to properly separate dropping the value from deallocating the memory. We'll explore Weak references in depth in Chapter 13 (Rc + RefCell). For this chapter, here's the complete Rc implementation:

#![allow(unused)]
fn main() {
use std::cell::Cell;
use std::ops::Deref;

struct RcInner<T> {
    strong_count: Cell<usize>,
    value: T,
}

pub struct Rc0<T> {
    ptr: *mut RcInner<T>,
}

impl<T> Rc0<T> {
    pub fn new(value: T) -> Rc0<T> {
        let inner = Box::new(RcInner {
            strong_count: Cell::new(1),
            value,
        });
        Rc0 { ptr: Box::into_raw(inner) }
    }

    pub fn strong_count(this: &Rc0<T>) -> usize {
        unsafe { (*this.ptr).strong_count.get() }
    }

    pub fn get_mut(this: &mut Rc0<T>) -> Option<&mut T> {
        if Rc0::strong_count(this) == 1 {
            unsafe { Some(&mut (*this.ptr).value) }
        } else {
            None
        }
    }
}

impl<T> Clone for Rc0<T> {
    fn clone(&self) -> Rc0<T> {
        let inner = unsafe { &*self.ptr };
        inner.strong_count.set(inner.strong_count.get() + 1);
        Rc0 { ptr: self.ptr }
    }
}

impl<T> Deref for Rc0<T> {
    type Target = T;
    fn deref(&self) -> &T {
        unsafe { &(*self.ptr).value }
    }
}

impl<T> Drop for Rc0<T> {
    fn drop(&mut self) {
        let inner = unsafe { &*self.ptr };
        let count = inner.strong_count.get();
        inner.strong_count.set(count - 1);

        if count == 1 {
            // Last reference - deallocate everything
            unsafe {
                drop(Box::from_raw(self.ptr));
            }
        }
    }
}
}

Rc vs Box vs References

Box<T>Rc<T>&T
OwnershipSingle ownerMultiple ownersBorrowed
Heap allocationYesYesNo
Clone behaviorDeep copyIncrement counterJust copy pointer
Runtime overheadNoneCounter checksNone
Mutability&mut via BoxNeed RefCellFollow borrows
Thread-safe?NoNoDepends on T
Use whenSingle ownerShared immutableShort-lived

Key Takeaways

  1. Rc enables shared ownership - Multiple owners can access the same data
  2. Clone is cheap - Only increments a counter, doesn't copy data (O(1))
  3. Only shared references - Rc gives &T, never &mut T (prevents data races)
  4. Beware of cycles - Rc cycles cause memory leaks (see Chapter 13 for solutions)
  5. Not thread-safe - Use Arc (Chapter 8) for multi-threaded code
  6. Automatic cleanup - RAII ensures memory is freed when last Rc drops
  7. get_mut confusion - get_mut() requires sole ownership, defeating Rc's purpose

Pitfalls

Pitfall 1: Cloning Inner Data Instead of Rc

BAD: Dereferencing and cloning the inner data:

#![allow(unused)]
fn main() {
let rc1 = Rc::new(vec![1, 2, 3, 4, 5]);

// ❌ This clones the Vec, not the Rc!
let vec_clone = (*rc1).clone();  // Expensive! Copies all elements
}

FIX: Clone the Rc, not the inner data:

#![allow(unused)]
fn main() {
let rc1 = Rc::new(vec![1, 2, 3, 4, 5]);

// ✅ This clones the Rc - cheap!
let rc2 = Rc::clone(&rc1);  // Just increments counter

// Both point to the same Vec
assert_eq!(Rc::strong_count(&rc1), 2);
}

Pitfall 2: Trying to Mutate Through Rc

BAD: Attempting to get &mut from Rc:

#![allow(unused)]
fn main() {
let rc = Rc::new(vec![1, 2, 3]);
// rc.push(4);  // ❌ ERROR: cannot borrow as mutable
}

Why this fails: Rc only provides shared references (&T), never mutable references (&mut T). This prevents data races when multiple owners exist.

FIX 1: If you're the sole owner, use get_mut():

#![allow(unused)]
fn main() {
let mut rc = Rc::new(vec![1, 2, 3]);
if let Some(vec) = Rc::get_mut(&mut rc) {
    vec.push(4);  // ✅ Works if strong_count == 1
}
}

FIX 2: Use Box or plain values if you don't need sharing:

#![allow(unused)]
fn main() {
// If you don't need shared ownership, don't use Rc!
let mut vec = vec![1, 2, 3];
vec.push(4);  // ✅ Simplest solution
}

Exercises

See examples/07_rc.rs for hands-on exercises demonstrating:

Complete solutions: Switch to the answers branch with git checkout answers to see completed exercises

For cycle prevention with Weak and mutable shared data, see Chapter 13 (Rc + RefCell).

Next Chapter

Chapter 8: Rc + RefCell - Shared mutable state with reference counting and interior mutability.