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.
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.
fnmain(){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
fnmain(){let boxed =Box::new(String::from("owned"));let address =(&*boxed)as*constString;let moved = boxed;assert_eq!((&*moved)as*constString, 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.
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
usestd::cell::Cell;usestd::rc::Rc;structTracked(Rc<Cell<usize>>);implDrop forTracked{fndrop(&mutself){self.0.set(self.0.get()+1);}}fnmain(){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.
Back-links Must Allow the Target to Have Disappeared
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.
Succeeds 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
usestd::cell::Cell;usestd::rc::Rc;#[derive(Clone)]structVersion{title: String,
counter:Rc<Cell<u32>>,
}fnmain(){letmut current =Rc::new(Version {
title:String::from("v1"), counter:Rc::new(Cell::new(0)),});let old =Rc::clone(¤t);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,¤t.counter));assert!(!Rc::ptr_eq(&old,¤t));}
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
usestd::rc::Rc;fnmain(){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.