What is a smart pointer?

A reference (&T) only borrows: it points to a value that somebody else owns. A smart pointer is a value that behaves like a pointer and also owns what it points to, often with extra bookkeeping, and cleans up when it goes out of scope. You have used two already: a String owns its bytes on the heap, a Vec<T> owns its elements. What makes a type a smart pointer is two standard traits. Deref lets *p and method calls reach the value inside, and Drop runs the cleanup code when the owner goes away (Drop is on Ownership).

The general ideas behind this page, the heap and reference counting, are on Memory Management. The standard library's smart pointers each answer one question:

Type Answers Checked
Box<T> "I need this value on the heap, with one owner." at compile time, like any owned value
Rc<T> "Several parts of one thread own this value together." counter at run time; read-only
Arc<T> "Several threads own this value together." atomic counter at run time; read-only
Weak<T> "I want to refer to it without keeping it alive." at run time: upgrade() may return None
RefCell<T>, Cell<T> "I need to change this through a shared reference." borrowing rules at run time (one thread only)

Box: one value on the heap

Box<T> is the simplest smart pointer: Box::new(value) moves the value to the heap and gives you a pointer to it, which is the box's only owner. When the box goes out of scope, the value is dropped and the heap memory freed. Apart from where the value lives, a box behaves like the value: it prints the same, compares the same through *, and you call the value's methods on it directly.

fn main() {
    let on_stack: u64 = 412_338;            // Wh, a meter total
    let on_heap: Box<u64> = Box::new(412_338);

    println!("{on_stack} {on_heap}");       // a Box prints like its contents
    println!("equal: {}", on_stack == *on_heap);   // * reaches the value inside

    let doubled = *on_heap * 2;
    println!("doubled {doubled}");
}   // on_heap goes out of scope: the Box frees its heap memory here
412338 412338
equal: true
doubled 824676

For a single number, a box is pointless; values on the stack are faster. A box earns its place in three situations: a type that contains itself, a value whose exact type is only known at run time (a trait object), and a large value you want to move around without copying all of its bytes.

Recursive types

The compiler must know the size of every type. A type that contains a value of itself directly would be infinitely large, so the compiler refuses it:

// A price rule that can contain other price rules (example values, in cents)
enum Rule {
    Fixed(u64),
    Plus(Rule, Rule),
}

fn main() {
    let _r = Rule::Fixed(9450);
}
example.rs:2:1: error[E0072]: recursive type `Rule` has infinite size
$ rustc --edition 2024 example.rs
error[E0072]: recursive type `Rule` has infinite size
 --> example.rs:2:1
  |
2 | enum Rule {
  | ^^^^^^^^^
3 |     Fixed(u64),
4 |     Plus(Rule, Rule),
  |          ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
4 |     Plus(Box<Rule>, Rule),
  |          ++++    +

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0072`.

A box has a fixed size, one pointer, whatever is inside, so it breaks the cycle. Here is a small price rule in which a rule can contain other rules, the shape of an expression tree. The €0.2700 per kWh is Kestrel's fixed tariff (350 kWh = €94.50); the monthly fee and the discount are example values:

// A price rule that can contain other price rules (amounts in cents)
enum Rule {
    Fixed(u64),
    Plus(Box<Rule>, Box<Rule>),
    Discount { percent: u64, of: Box<Rule> },
}

fn cents(rule: &Rule) -> u64 {
    match rule {
        Rule::Fixed(c) => *c,
        Rule::Plus(a, b) => cents(a) + cents(b),
        Rule::Discount { percent, of } => cents(of) * (100 - percent) / 100,
    }
}

fn main() {
    // 350 kWh at €0.2700 plus a monthly fee, with a 10% discount on the total
    let bill = Rule::Discount {
        percent: 10,
        of: Box::new(Rule::Plus(
            Box::new(Rule::Fixed(9450)),
            Box::new(Rule::Fixed(550)),
        )),
    };
    let c = cents(&bill);
    println!("€{}.{:02}", c / 100, c % 100);
}
€90.00

cents(a) receives a &Box<Rule> where a &Rule is expected and the compiler converts it automatically. That conversion is the subject of the next section.

How big is a smart pointer?

On a 64-bit Linux machine (ARM64), where this page's examples were run, a pointer is 8 bytes. Sizes are platform-specific: a 32-bit target has 4-byte pointers.

use std::cell::RefCell;
use std::mem::size_of;
use std::rc::Rc;
use std::sync::Arc;

fn show<T>(name: &str) {
    println!("{name:<17}{:>3} bytes", size_of::<T>());
}

fn main() {
    show::<u64>("u64");
    show::<Box<u64>>("Box<u64>");
    show::<Option<Box<u64>>>("Option<Box<u64>>");
    show::<Rc<u64>>("Rc<u64>");
    show::<Arc<u64>>("Arc<u64>");
    show::<RefCell<u32>>("RefCell<u32>");
    show::<String>("String");
    show::<Box<dyn Fn()>>("Box<dyn Fn()>");
}
u64                8 bytes
Box<u64>           8 bytes
Option<Box<u64>>   8 bytes
Rc<u64>            8 bytes
Arc<u64>           8 bytes
RefCell<u32>      16 bytes
String            24 bytes
Box<dyn Fn()>     16 bytes

Box, Rc and Arc are a single pointer; the counters of Rc and Arc live on the heap next to the value. Option<Box<u64>> is still 8 bytes, because a box can never be null, so the compiler uses the null value to mean None. RefCell<u32> is larger than the u32 because it stores its borrow state next to the value. Box<dyn Fn()> is a "fat" pointer: the address of the closure plus the address of a table with its methods (see trait objects on Traits and Generics).

Deref and deref coercion

The * operator follows a reference to its value. A smart pointer implements the Deref trait, so *boxed reaches the value inside a box the same way. Rust also applies Deref by itself in two places. A method call such as boxed.len() follows pointers until it finds the method. And deref coercion converts a reference to a smart pointer into a reference to what it points to, whenever a function expects that: a &String becomes a &str, and a &Box<String> becomes a &String and then a &str.

use std::rc::Rc;

fn shout(text: &str) -> String {
    text.to_uppercase()
}

fn main() {
    let literal: &str = "m-30004";
    let owned: String = String::from("m-30004");
    let boxed: Box<String> = Box::new(String::from("m-30004"));
    let shared: Rc<String> = Rc::new(String::from("m-30004"));

    // shout wants a &str; each of these turns into one automatically
    println!("{}", shout(literal));
    println!("{}", shout(&owned));      // &String         -> &str
    println!("{}", shout(&boxed));      // &Box<String>    -> &String -> &str
    println!("{}", shout(&shared));     // &Rc<String>     -> &String -> &str

    // methods work through the pointer too
    println!("{} {} {}", owned.len(), boxed.len(), shared.len());
}
M-30004
M-30004
M-30004
M-30004
7 7 7

This is why functions should take &str rather than &String, and &[T] rather than &Vec<T>: callers can then pass a literal, a String, a boxed string or a shared one without converting anything. Deref coercion happens only for references, only at compile time, and costs nothing at run time. A DerefMut trait does the same for &mut; Rc and Arc do not implement it, as the next section shows.

Rc: shared ownership in one thread

Ownership normally has one owner. Sometimes a value really has several: one tariff table used by many contracts, a configuration shared by several parts of a program, a node in a graph that several other nodes point to. No single one of them should free it; it should be freed when the last one is done with it. Rc<T> (reference counted) does exactly that. Rc::new puts the value on the heap next to a counter, Rc::clone(&rc) makes another owning handle and adds one to the count, and every handle that is dropped subtracts one. When the count reaches zero, the value is dropped.

use std::rc::Rc;

struct Tariff {
    name: String,
    price: u64, // ten-thousandths of a euro per kWh
}

struct Contract {
    number: u32,
    tariff: Rc<Tariff>,
}

fn main() {
    let fixed = Rc::new(Tariff { name: String::from("fixed"), price: 2700 });
    println!("count after new: {}", Rc::strong_count(&fixed));

    let a = Contract { number: 1, tariff: Rc::clone(&fixed) };
    let b = Contract { number: 2, tariff: Rc::clone(&fixed) };
    println!("count with two contracts: {}", Rc::strong_count(&fixed));

    for c in [&a, &b] {
        println!("contract {} pays {} at {}", c.number, c.tariff.name, c.tariff.price);
    }

    drop(a);
    println!("count after dropping one: {}", Rc::strong_count(&fixed));
    println!("same tariff: {}", Rc::ptr_eq(&fixed, &b.tariff));
}
count after new: 1
count with two contracts: 3
contract 1 pays fixed at 2700
contract 2 pays fixed at 2700
count after dropping one: 2
same tariff: true

The count is 3 with two contracts because fixed itself is a handle too. Rc::clone copies only the pointer and increases the counter; the tariff is not copied, which Rc::ptr_eq confirms. (Writing fixed.clone() does the same, but Rc::clone(&fixed) makes it visible to a reader that no deep copy happens.)

Box and Rc in memory. On the stack, a variable on_heap of type Box points to one heap block that holds the value 412338. Below it, two stack variables, fixed and b.tariff, both point to the same heap block. That block holds three parts: a strong count of 2, a weak count of 0, and the Tariff value. stack heap on_heap 412338 Box: one owner fixed b.tariff strong 2 weak 0 Tariff Rc: the counts live next to the value
A Box owns its value alone. An Rc allocation keeps two counters next to the value; every Rc::clone adds one to the strong count. An Arc looks the same, with atomic counters.

Because several handles may read the value at the same time, Rc hands out shared references only. Changing the value through it is refused at compile time:

use std::rc::Rc;

fn main() {
    let price = Rc::new(2700u64);
    let other = Rc::clone(&price);
    *other += 100;
    println!("{price}");
}
example.rs:6:5: error[E0594]: cannot assign to data in an `Rc`: cannot assign

The full message adds: "trait DerefMut is required to modify through a dereference, but it is not implemented for Rc<u64>". When shared data really must change, combine Rc with a RefCell (below).

Weak references and reference cycles

Reference counting has one weakness, in every language that uses it: two values that hold strong references to each other keep each other's count above zero forever, even when nothing else refers to either of them. Rust does not stop you from building such a cycle. The memory leaks, and the Drop code never runs:

use std::cell::RefCell;
use std::rc::Rc;

struct Meter {
    id: &'static str,
    next: RefCell<Option<Rc<Meter>>>, // a link to another meter
}

impl Drop for Meter {
    fn drop(&mut self) {
        println!("freed {}", self.id);
    }
}

fn main() {
    {
        let a = Rc::new(Meter { id: "M-1", next: RefCell::new(None) });
        let b = Rc::new(Meter { id: "M-2", next: RefCell::new(None) });
        *b.next.borrow_mut() = Some(Rc::clone(&a)); // b -> a
        println!("a count {}, b count {}", Rc::strong_count(&a), Rc::strong_count(&b));
    }
    println!("end of first block");

    {
        let a = Rc::new(Meter { id: "M-3", next: RefCell::new(None) });
        let b = Rc::new(Meter { id: "M-4", next: RefCell::new(None) });
        *b.next.borrow_mut() = Some(Rc::clone(&a)); // b -> a
        *a.next.borrow_mut() = Some(Rc::clone(&b)); // a -> b: a cycle
        println!("a count {}, b count {}", Rc::strong_count(&a), Rc::strong_count(&b));
    }
    println!("end of second block (M-3 and M-4 were never freed)");
}
a count 2, b count 1
freed M-2
freed M-1
end of first block
a count 2, b count 2
end of second block (M-3 and M-4 were never freed)

In the first block, b goes out of scope first and is freed, which drops its link to a and lowers a's count to 1; when a goes out of scope right after, its count reaches 0 and it is freed too. In the second block each meter keeps the other's count at 1 after both variables are gone. The Rust Reference lists memory leaks among the behavior that is "not considered unsafe": a leak wastes memory but cannot corrupt it. It is still a bug, in a long-running agent or service especially.

The fix is to decide which direction owns. A weak reference, Weak<T>, made with Rc::downgrade(&rc), points to the same allocation but adds to the separate weak count, which does not keep the value alive. Before you use it, upgrade() turns it back into an Rc: Some(rc) while the value exists, None after it was dropped. The usual shape is a tree: parents own their children with Rc, children point back to their parent with Weak.

use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Home {
    id: &'static str,
    meters: RefCell<Vec<Rc<Meter>>>, // the home owns its meters
}

struct Meter {
    id: &'static str,
    home: Weak<Home>, // a meter only refers back to its home
}

fn main() {
    let home = Rc::new(Home { id: "h0042", meters: RefCell::new(Vec::new()) });
    let meter = Rc::new(Meter { id: "M-1", home: Rc::downgrade(&home) });
    home.meters.borrow_mut().push(Rc::clone(&meter));

    println!("strong {}, weak {}", Rc::strong_count(&home), Rc::weak_count(&home));
    println!("{} has {} meter(s)", home.id, home.meters.borrow().len());

    match meter.home.upgrade() {
        Some(h) => println!("{} belongs to {}", meter.id, h.id),
        None => println!("{} has no home any more", meter.id),
    }

    drop(home); // the last strong reference: Home is freed (and its Vec with it)

    match meter.home.upgrade() {
        Some(h) => println!("{} belongs to {}", meter.id, h.id),
        None => println!("{} has no home any more", meter.id),
    }
}
strong 1, weak 1
h0042 has 1 meter(s)
M-1 belongs to h0042
M-1 has no home any more

The home owns the meter (strong), the meter only knows its home (weak). Dropping the last strong handle to the home frees it even though the meter still points back, and the meter can tell: upgrade() returns None instead of a dangling pointer.

Interior mutability: RefCell and Cell

The borrowing rules say: many shared references or one mutable reference, never both (see Borrowing and References). The compiler checks them on the source code, and sometimes the source code does not show enough: a value shared through Rc, or a method that takes &self but needs to update a cache or a log. Interior mutability is the pattern for these cases: a type that allows changing its contents through a shared reference, and that enforces safety some other way.

RefCell: borrowing checked at run time

RefCell<T> moves the borrow check from compile time to run time. borrow() returns a guard that acts like &T, borrow_mut() one that acts like &mut T, and the RefCell counts the guards that are alive. The rules are the same as at compile time; only the moment of checking differs. In this example, a developer on Kestrel's platform team writes a Rust prototype of the parser from BUG-1060, to compare with the manufacturer's C fix. It keeps a log of the CRC fields it rejected, through &self:

use std::cell::RefCell;

struct Parser {
    rejected: RefCell<Vec<String>>, // a log the parser may add to through &self
}

impl Parser {
    // &self, not &mut self: callers can share the parser
    fn sent_crc(&self, tail: &str) -> Option<u16> {
        let digits = tail.strip_suffix("\r\n").unwrap_or(tail);
        let ok = !digits.is_empty()
            && digits.len() <= 4
            && digits.bytes().all(|b| b.is_ascii_hexdigit());
        if !ok {
            self.rejected.borrow_mut().push(digits.to_string());
            return None;
        }
        u16::from_str_radix(digits, 16).ok()
    }
}

fn main() {
    let parser = Parser { rejected: RefCell::new(Vec::new()) };
    for tail in ["F1F4\r\n", "B9F\r\n", "+B9F\r\n", "F1F4F\r\n"] {
        println!("{:?} -> {:04X?}", tail.trim_end(), parser.sent_crc(tail));
    }
    println!("rejected: {:?}", parser.rejected.borrow());
}
"F1F4" -> Some(F1F4)
"B9F" -> Some(0B9F)
"+B9F" -> None
"F1F4F" -> None
rejected: ["+B9F", "F1F4F"]

The three-digit CRC B9F that a meter really sent (BUG-1060) is accepted as 0B9F; +B9F and the five-digit value are rejected. The explicit digit check matters: u16::from_str_radix alone accepts a leading +, so the type system does not replace validation. The guard from borrow_mut() lives only until the end of the push statement. Keep a guard alive while asking for a conflicting one, and the program stops:

use std::cell::RefCell;

fn main() {
    let log = RefCell::new(vec![350]);

    let first = log.borrow();          // a shared borrow, checked at run time
    println!("first {}", first[0]);
    log.borrow_mut().push(412);        // a mutable borrow while `first` still lives
    println!("{}", first.len());
}
first 350
thread 'main' (3199224) panicked at example.rs:8:9:
RefCell already borrowed

Where the compiler would have refused the program, a RefCell panics at run time. The cost of the flexibility is that a mistake shows up while the program runs, on the path that triggers it, and not when you build it. Two habits keep that rare: keep guards short-lived (inside a small block, or in a single expression), and use try_borrow() and try_borrow_mut(), which return a Result instead of panicking, where a conflict is possible:

use std::cell::RefCell;

fn main() {
    let log = RefCell::new(vec![350]);

    {
        let first = log.borrow();
        println!("first {}", first[0]);
        println!("try_borrow_mut while reading: {:?}", log.try_borrow_mut().is_ok());
    } // `first` ends here, and with it the shared borrow

    log.borrow_mut().push(412);
    println!("after: {:?}", log.borrow());
}
first 350
try_borrow_mut while reading: false
after: [350, 412]

Cell: for small Copy values

Cell<T> is the simpler sibling. It never hands out references to its contents at all: get() returns a copy, set() replaces the value. Without references there is nothing to check, so Cell has no run-time borrow state and cannot panic. It suits counters and flags: get() needs a Copy type such as a number or a bool.

use std::cell::Cell;

struct Meter {
    id: &'static str,
    reads: Cell<u32>, // how often this meter was read
}

impl Meter {
    fn read(&self) -> u32 {
        self.reads.set(self.reads.get() + 1);
        1234 // watts
    }
}

fn main() {
    let meter = Meter { id: "M-1", reads: Cell::new(0) };
    let total: u32 = (0..3).map(|_| meter.read()).sum();
    println!("{} read {} times, total {total} W", meter.id, meter.reads.get());
}
M-1 read 3 times, total 3702 W

Rc and RefCell together

Rc gives several owners, RefCell gives changes through shared references; together, Rc<RefCell<T>> is a value that several parts of one thread own and change. In this example, two parts of a program add to one shared list of alerts:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    // one shared, changeable list of alerts
    let alerts = Rc::new(RefCell::new(Vec::new()));

    let from_parser = Rc::clone(&alerts);
    let from_broker = Rc::clone(&alerts);

    from_parser.borrow_mut().push("parser: CRC mismatch");
    from_broker.borrow_mut().push("broker: reconnected");

    println!("{} handles, {} alerts", Rc::strong_count(&alerts), alerts.borrow().len());
    for a in alerts.borrow().iter() {
        println!("{a}");
    }
}
3 handles, 2 alerts
parser: CRC mismatch
broker: reconnected

Arc: shared ownership across threads

Rc updates its counter with ordinary instructions. If two threads cloned or dropped the same Rc at the same moment, the counter could go wrong and the value could be freed while still in use. Rust does not let that happen: a type that is not safe to move to another thread does not implement the marker trait Send, and thread::spawn requires it. The mistake is caught when you compile:

use std::rc::Rc;
use std::thread;

fn main() {
    let tariff = Rc::new(String::from("fixed"));
    let copy = Rc::clone(&tariff);
    let worker = thread::spawn(move || println!("worker sees {copy}"));
    worker.join().unwrap();
    println!("main sees {tariff}");
}
example.rs:7:32: error[E0277]: `Rc<String>` cannot be sent between threads safely: `Rc<String>` cannot be sent between threads safely

Arc<T> (atomically reference counted) is the thread-safe version. It has the same interface as Rc, but updates the counters with atomic CPU instructions, which are slightly slower; that is why both exist. Each thread gets its own handle through Arc::clone:

use std::sync::Arc;
use std::thread;

fn main() {
    let tariff = Arc::new(String::from("fixed"));

    let workers: Vec<_> = (1..=3)
        .map(|n| {
            let tariff = Arc::clone(&tariff); // one handle per thread
            thread::spawn(move || format!("worker {n} sees {tariff}"))
        })
        .collect();

    for w in workers {
        println!("{}", w.join().unwrap()); // joined, and printed, in order
    }
    println!("handles left: {}", Arc::strong_count(&tariff));
}
worker 1 sees fixed
worker 2 sees fixed
worker 3 sees fixed
handles left: 1

The threads only read the tariff, and that is all an Arc allows on its own. For data that threads change, the thread-safe counterpart of RefCell is a Mutex or RwLock, usually as Arc<Mutex<T>>: see Concurrency, which also explains Send and Sync.

Choosing a smart pointer

You need One thread Several threads
one owner, value on the heap Box<T> Box<T> (moved to the thread)
several owners, read-only Rc<T> Arc<T>
a link that must not keep the value alive rc::Weak<T> sync::Weak<T>
change through a shared reference RefCell<T>, Cell<T> Mutex<T>, RwLock<T>, atomics
several owners that change the value Rc<RefCell<T>> Arc<Mutex<T>>

Start with plain ownership and references, and add a smart pointer when the compiler shows you why you need one. Each step down the table moves a check from compile time to run time, or adds a small cost.