Box, Rc, and Arc

On this page

These types first determine who is responsible for keeping a value alive and who eventually frees it. Box expresses a single owner; Rc and Arc express multiple shared owners. Whether internal values need to be modified or synchronized across threads is a separate set of concerns.

TypeOwnershipMeaning of CloneTypical Use Case
BoxExclusiveClones the inner value to a new Box when T: CloneRecursive structures, owning DSTs, independent allocation
RcShared, non-atomic countIncrements strong count, does not clone TSingle-threaded shared structures
ArcShared, atomic countIncrements strong count, does not clone TShared ownership satisfying thread constraints
WeakDoes not keep T aliveIncrements weak countBack-links, non-owning observers

Box Turns Recursive Size into Indirection

If a node directly contains the next node of the same type, its size recursively depends on itself. By holding the next node indirectly via Box, the size of the current node becomes determinable. Box<dyn Trait> and Box<[T]> can also own values whose concrete size is unknown at compile time.

For Sized T, a standard Box<T> has a single-pointer representation guarantee, but the pointer width depends on the target platform; DST pointers also carry corresponding metadata, so they cannot be uniformly called "8 bytes." A Box of a zero-sized type does not need to allocate non-zero heap space for T. Box

Box::new(large_array) expresses that the heap value is ultimately owned, but it does not guarantee that no large array stack temporaries exist during construction. Large byte buffers can be constructed from a Vec and then converted via into_boxed_slice; do not infer peak stack usage solely from seeing a single pointer in the source code.

fn main() {
    let bytes: Box<[u8]> = vec![0; 1_000_000].into_boxed_slice();
    assert_eq!(bytes.len(), 1_000_000);
    let moved = bytes; // Move the owner; the byte buffer is not relocated as a whole
    assert_eq!(moved[0], 0);
}

Moving the Box itself does not mean the inner value is fixed: through a valid &mut T, you can still replace or move out T. When an address-stability contract is needed, Pin is involved; see Memory Layout.

Moving the Box vs. Moving the Value Inside the Box

fn main() {
    let boxed = Box::new(String::from("owned"));
    let address = (&*boxed) as *const String;
    let moved = boxed;
    assert_eq!((&*moved) as *const String, address);
    let value: String = *moved;
    assert_eq!(value, "owned");
}

When moving the Box handle, the heap-allocated String object in the example remains in place; subsequently, *moved takes ownership of the String, consuming the Box. The byte buffer owned by String and the String object itself on the heap should be viewed separately. The address being the same before and after a single handle move does not prove that T is immovable forever, nor can it replace the contract of Pin.

Recursive structures should also consider the drop path. Box solves the infinite recursion of type size, but it does not automatically eliminate the call depth of long chains of layered Drop calls; very long linked lists may require explicit iterative dismantling. If it is just contiguous elements, Vec is often more direct in layout and traversal.

Rc and Arc Share the Same Value

Rc::clone(&a) and Arc::clone(&a) create new ownership handles, usually not requiring T: Clone. How the value and count are laid out are implementation details; you should not draw stable ABI field byte counts from a specific platform.

Rc is neither Send nor Sync. Arc's count can be coordinated across threads, but for Arc<T> with the default allocator, implementing Send and Sync still requires T: Send + Sync. Arc<RefCell<T>> does not automatically gain thread-safe interior mutability. Arc

The cost of Arc's atomic count is affected by architecture, contention, and cache; you cannot give a universal conclusion that it is "10 times slower than Rc." Whether frequent cloning is significant should be measured against actual hot paths; if borrowing &T is sufficient, there is no need to create an owner every time.

Value Equality vs. Same Shared Object

use std::rc::Rc;
fn main() {
    let first = Rc::new(String::from("same"));
    let shared = Rc::clone(&first);
    let independent = Rc::new(String::from("same"));
    assert_eq!(first, independent); // Content equality
    assert!(Rc::ptr_eq(&first, &shared));
    assert!(!Rc::ptr_eq(&first, &independent));
    assert_eq!(Rc::strong_count(&first), 2);
    drop(shared);
    assert_eq!(Rc::strong_count(&first), 1);
}

Rc::clone explicitly copies the ownership handle; (*first).clone() calls String's Clone. The latter produces another String, but Clone for general types does not necessarily deep-copy all reachable data; for example, a struct containing an Arc field may still share that field. Therefore, "cloning T" does not equal "getting a completely isolated object graph."

When a function only reads data during the call, accepting &T is sufficient; only when you need to retain shared ownership should you accept or clone Rc/Arc to express the corresponding contract. Changing all parameters to Arc<T> propagates allocation and sharing policies to interfaces that do not need them.

Weak: Does Not Keep Value Alive, But Allows Safe Attempt to Acquire Ownership

At least one strong referenceT is alive; Weak can upgrade to get a strong reference1Last strong reference is releasedT is destructed; existing Weak upgrades return None2Last Weak is also released3Value Lifetime and Reference Count Allocation LifetimeRemaining allocation used by reference count can be reclaimedSchematic of standard Rc/Arc lifecycles; Weak does not keep T alive, nor is it equivalent to a raw pointer.
use std::cell::Cell;
use std::rc::Rc;

struct Tracked(Rc<Cell<usize>>);
impl Drop for Tracked {
    fn drop(&mut self) { self.0.set(self.0.get() + 1); }
}

fn main() {
    let drops = Rc::new(Cell::new(0));
    let first = Rc::new(Tracked(Rc::clone(&drops)));
    let second = Rc::clone(&first);
    let observer = Rc::downgrade(&first);
    drop(first);
    assert!(observer.upgrade().is_some());
    assert_eq!(drops.get(), 0);
    drop(second);
    assert_eq!(drops.get(), 1);
    assert!(observer.upgrade().is_none());
}

The new strong reference returned successfully by upgrade keeps T alive during its lifetime; failure returns None, rather than exposing a dereferenceable dangling pointer. When the last strong reference is released, T is already destructed, even if Weak still keeps the underlying allocation alive.

A common tree model is parent nodes strongly referencing child nodes, with child nodes using Weak to point to parent nodes. The key is not which direction must be weak, but that the ownership graph must not allow nodes that are no longer needed to keep each other alive via strong reference cycles. Only parent-to-child strong references do not form a cycle; bidirectional strong references may form a cycle.

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
    name: &'static str,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
    fn new(name: &'static str) -> Rc<Self> {
        Rc::new(Self {
            name, parent: RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }
}
fn main() {
    let child = Node::new("child");
    {
        let parent = Node::new("parent");
        *child.parent.borrow_mut() = Rc::downgrade(&parent);
        parent.children.borrow_mut().push(Rc::clone(&child));
        let owner = child.parent.borrow().upgrade().unwrap();
        assert_eq!(owner.name, "parent");
    }
    assert!(child.parent.borrow().upgrade().is_none());
    assert_eq!(child.name, "child");
}

child has an external strong owner, so it survives after parent is destroyed; its parent link can no longer be upgraded. This is a normal state allowed by Weak, and the caller should handle None. The example only shows one connection; a complete tree API also needs to maintain lists of old parent nodes, prevent ancestors from becoming descendants, and other structural invariants; Weak itself does not verify these business rules.

Weak is also not an auto-cleaning cache. If all values in a HashMap are Weak, keys and weak handles may still remain in the table after the target disappears; you need to delete them or clean them up regularly when upgrade fails. Lifetime and cache capacity are different issues.

Value destruction and allocation release differ

Create a Weak, then drop all strong handles: T disappears while its control allocation remains for Weak. An upgrade now returns None; dropping the final Weak permits deallocation. When a strong owner still exists, successful upgrade creates another strong owner to manage.

Preparing the visual
Value destruction and allocation release differ

The last strong reference drops T; remaining Weak handles retain the allocation but cannot resurrect T.

Modifying Shared Values: First Clarify Whether Other Owners Should See It

RequirementMethodConsequence
Already exclusively owned, modify directlyget_mutNo cloning; returns None if other strong or weak references exist
Modify your own versionmake_mutClones T if multiple strong references exist; old owners still see the old value
Modify state seen by all ownersRefCell, Mutex, etc. (interior mutability)Keeps shared object, constrains access additionally
Consume the last strong owner, extract Ttry_unwrapSucceeds if strong count is 1; existing Weak does not prevent success

For Rc/Arc's make_mut, if there is only one strong reference but also Weak, it detaches these Weak from the newly held value without cloning T; Weak subsequently cannot upgrade to the modified value. This is different from get_mut's behavior of directly rejecting. Rc::make_mut

use std::rc::Rc;

fn main() {
    let mut current = Rc::new(String::from("v1"));
    let snapshot = Rc::clone(&current);
    Rc::make_mut(&mut current).push_str("+edit");
    assert_eq!(&*snapshot, "v1");
    assert_eq!(&*current, "v1+edit");

    let weak = Rc::downgrade(&current);
    assert!(Rc::get_mut(&mut current).is_none());
    Rc::make_mut(&mut current).push('!');
    assert!(weak.upgrade().is_none());

    let another_weak = Rc::downgrade(&current);
    assert_eq!(Rc::try_unwrap(current).unwrap(), "v1+edit!");
    assert!(another_weak.upgrade().is_none());
}
Only one strong referenceAnd no WeakMultiple strong ownersOther owners retain old versionOne strong + WeakWeak observers do not keep T aliveDirectly get &mut TDo not clone TClone T, modify cloneOther owners keep originalDetach from existing WeakMove T; detach old Weakmake_mut: Decide Whether to Copy Based on Current OwnershipThis is the behavior branch of make_mut; get_mut does not copy or detach from Weak on behalf of the caller.

Copy-on-Write Is Not Necessarily a Deep Snapshot

use std::cell::Cell;
use std::rc::Rc;
#[derive(Clone)]
struct Version {
    title: String,
    counter: Rc<Cell<u32>>,
}
fn main() {
    let mut current = Rc::new(Version {
        title: String::from("v1"), counter: Rc::new(Cell::new(0)),
    });
    let old = Rc::clone(&current);
    Rc::make_mut(&mut current).title.push_str(" edited");
    current.counter.set(7);
    assert_eq!(old.title, "v1");
    assert_eq!(current.title, "v1 edited");
    assert_eq!(old.counter.get(), 7);
    assert!(Rc::ptr_eq(&old.counter, &current.counter));
    assert!(!Rc::ptr_eq(&old, &current));
}

The outer Version has been separated, but the derived Clone only copies Rc for counter, so both versions still see the same counter. If product semantics require a complete immutable snapshot, you need to ensure fields themselves are immutable, or explicitly clone each layer of data; simply wrapping in an outer Rc and calling make_mut is not enough to establish this guarantee.

When Taking Back T Fails, You Still Hold the Original Ownership

use std::rc::Rc;
fn main() {
    let first = Rc::new(String::from("keep me"));
    let second = Rc::clone(&first);
    let first = Rc::try_unwrap(first).expect_err("There is another strong owner");
    assert!(Rc::ptr_eq(&first, &second));
    drop(second);
    assert_eq!(Rc::try_unwrap(first).unwrap(), "keep me");
}

If try_unwrap fails, it returns the original handle in Err; the caller can continue to hold it, rather than having to discard the shared object. In concurrent scenarios with Arc, be aware: multiple threads executing try_unwrap(...).ok() may all fail to get T, and then each discards Err, eventually releasing T via destructor. If you want all strong owners to participate in the "exactly one takes the value" protocol, you can study the corresponding guarantees of Arc::into_inner and ensure each strong handle follows the protocol. Arc::into_inner

Do not check Arc::strong_count first and then use unsafe to create &mut T: the count query may change between other thread operations, and it also involves Weak. Use encapsulated contracts like get_mut. For specific choices of interior mutability, see Cell, RefCell, and Mutex.