Smart Pointers
Smart pointers are special data structures that not only act as pointers (to manage references to data) but also include additional functionality, such as automatic memory management.
Smart Pointer | Description |
---|---|
Box<T> | A smart pointer for allocating values on the heap. Useful when you have a large amount of data or a value whose size is not known at compile time. |
Rc<T> | A reference-counted smart pointer used for sharing ownership of data. Best for scenarios where multiple parts of your program need access to the same data. Not thread-safe; use for thread-safe sharing. |
Arc<T> | Uses atomic operations to ensure safe concurrent access. Similar to but thread-safe, enabling shared ownership across threads. |
RefCell<T> | Allows interior mutability (inside an immutable reference). Enforces borrow checking at runtime rather than compile time. Works well with when shared data requires interior mutability. |
Cell<T> | Similar to , but with fewer safety checks and restrictions. Enables interior mutability for types without borrowing. |
Comparison
Rc<T>
enables multiple owners of the same data;Box<T>
andRefCell<T>
have single owners.Box<T>
allows immutable or mutable borrows checked at compile time;Rc<T>
allows only immutable borrows checked at compile time;RefCell<T>
allows immutable or mutable borrows checked at runtime.- Because
RefCell<T>
allows mutable borrows checked at runtime, you can mutate the value inside theRefCell<T>
even when theRefCell<T>
is immutable.
Box
All values in Rust are stack-allocated by default. Box<T>
allow you to store data on the heap rather than the stack. What remains on the stack is the pointer to the heap data.
Boxes provide ownership for this allocation, and drop their contents when they go out of scope. Boxes also ensure that they never allocate more than isize::MAX
bytes.
The Box<T>
type is a smart pointer, because it implements the std::ops::Deref
⮳ trait, which allows Box<T>
values to be treated like a reference. You can use the de-reference operator *
or 'deref coercion' with the .
operator to retrieve its inner value.
let boxed: Box<u8> = Box::new(1); let _val: u8 = *boxed; let boxed = Box::new("example"); // Deref coercion: equivalent to (*boxed.deref()).len() let _val = boxed.len();
Use Box<T>
when
- you have a dynamically sized type, whose size can't be known at compile time.
- you want to own a value and you care only that it's a type that implements a particular trait rather than being of a specific type.
- you don't want to rely on stack space.
/// Represents a single element in a linked list. struct Node { /// The value stored in this node. value: i32, /// The next node in the list, or `None` if this is the last node. /// `Node` is a recursive data type, so we use `Box` to store it on the /// heap. next: Option<Box<Node>>, } impl Node { fn new(value: i32) -> Self { Node { value, next: None } } /// Recursively traverses the list until it finds the last node /// (where next is `None`) and sets its next field to a new `Node`. /// /// # Arguments /// /// * `value` - The value to be stored in the new node. fn append(&mut self, value: i32) { match self.next { Some(ref mut next_node) => next_node.append(value), None => self.next = Some(Box::new(Node::new(value))), } } /// Prints the values of the nodes in the list, separated by " -> ". fn print(&self) { print!("{}", self.value); if let Some(ref next_node) = self.next { print!(" -> "); next_node.print(); } else { println!(); } } } fn main() { // The linked list has an unknown number of nodes, thus its size is not // fixed. It could not be stored directly on the stack, because the // compiler needs to know the size of the data type at compile time. // By using `Box`, which is a pointer to the heap and has a defined size, we // can create the `head` local variable on the stack. // The actual `Node` data will be stored on the heap. let mut head = Node::new(1); head.append(2); head.append(3); head.append(4); head.print(); // Output: 1 -> 2 -> 3 -> 4. }
Rc
The Rc<T>
type (for "Reference Counted") enables shared ownership of a value.
Rc
maintains a reference count of the number of owners. You can create additional references to the data using theclone
⮳ method. Cloning anRc
only increments the reference count without duplicating the data. When the last owner goes out of scope, the data is automatically cleaned up (dropped).- If you need mutability, put a
Cell
orRefCell
inside theRc
. Rc
automatically dereferences toT
(via theDeref
⮳ trait), so you can callT
's methods on a value of typeRc<T>
.Rc
is commonly used in data structures, such as graphs and linked lists, where multiple nodes might need to share ownership of certain nodes or data.
Keep in mind that Rc
is not thread-safe. For concurrent scenarios, you should use Arc
(Atomic Reference Counted), which provides similar functionality with thread safety.
//! `Rc` stands for "Reference Counted". //! //! `Rc` is a smart pointer that allows multiple owners of the same data. //! `Rc` keeps track of the number of owners of the data. //! When the last owner is dropped, the data is deallocated. use std::rc::Rc; fn main() { let a = Rc::new(vec![1.0, 2.0, 3.0]); // The two syntaxes below are equivalent for cloning an `Rc`. let b = a.clone(); let c = Rc::clone(&a); // `b` and `c` now both point to the same memory location as `a`. // Gets the number of (Rc) pointers to this allocation. assert_eq!(3, Rc::strong_count(&a)); // Dropping one of the pointers decrements the strong reference count. drop(c); assert_eq!(2, Rc::strong_count(&a)); // `Rc` is a smart pointer, so we can dereference it. println!("{:?}", *a); b.iter().for_each(|x| print!("{} ", x)); // We can get a mutable reference to the inner value if there are no other // `Rc` or `Weak` pointers to the same allocation. Returns `None` otherwise. let mut y = Rc::new(4); *Rc::get_mut(&mut y).unwrap() = 5; assert_eq!(*y, 5); // We can also consume the `Rc` to return its inner value if the `Rc` has // exactly one strong reference. See also `Rc::into_inner`. let x = Rc::new(6); assert_eq!(Rc::try_unwrap(x), Ok(6)); }
Also of note: the Weak
type, typically obtained via Rc::downgrade
, allows for non-owning (weak) references to the data. This can help prevent reference cycles that could lead to memory leaks.
RefCell
Rust memory safety allows (i) several immutable references (&T
) to an object T
; or (ii) one mutable reference (&mut T
). This is enforced at compile time. However, sometimes it is required to have multiple references to an object and yet mutate it. RefCell<T>
(and related types Cell<T>
and OnceCell<T>
) have interior mutability, a pattern that allows you to mutate data even when there are immutable references to it.
These types are used in scenarios involving shared state within a single thread, like GUI applications, or when creating complex data structures like graphs.
RefCell
keeps track of borrowing rules at runtime and ensures that only one mutable or multiple immutable borrows exist at a time. Attempts to violate borrowing rules (like having multiple mutable borrows) will cause a panic at runtime. Common methods includeborrow
,borrow_mut
⮳, andtry_borrow
.
//! `RefCell<T>` is a smart pointer that provides interior mutability. //! It allows you to mutate data even when there are immutable references to it. //! It enforces borrowing rules at runtime, panicking if they are violated. //! //! `RefCell<T>` is useful when you need to modify data that is behind an //! immutable reference. use std::cell::RefCell; fn main() { // Create a RefCell containing a vector of integers. let data = RefCell::new(vec![1, 2, 3, 4, 5]); // Borrow the data immutably. { let borrowed_data = data.borrow(); println!("Borrowed (immutable): {:?}", borrowed_data); // Multiple immutable borrows are allowed. // We can borrow the data immutably again here. let _borrowed_data2 = data.borrow(); } // The immutable borrow ends here (when the `Ref` guard returned by `borrow` // exits scope.) // Borrow the data mutably and modify it. { let mut borrowed_data = data.borrow_mut(); borrowed_data.push(6); println!("Borrowed (mutable): {:?}", borrowed_data); // Only one mutable borrow is allowed at a time. // We can't borrow the data again while it's borrowed mutably. This // would panic if we used `borrow` or `borrow_mut`. assert!(data.try_borrow().is_err()); assert!(data.try_borrow_mut().is_err()); } // The mutable borrow ends here. // Borrow the data immutably again to check the modification. { if let Ok(borrowed_data) = data.try_borrow() { println!("Borrowed (immutable again): {:?}", borrowed_data); } // `try_borrow` returns a `Result` to handle the case where the data is // already borrowed mutably. } // We can also consume the `RefCell`, returning the wrapped value. let _data = data.into_inner(); }
RefCell<T>
(andCell<T>
,OnceCell<T>
) do not implementSync
and are therefore single-threaded. The correspondingSync
version ofRefCell<T>
isRwLock<T>
. UseMutex<T>
,RwLock<T>
,OnceLock<T>
, or atomic types when working with multiple threads.
Cell
Cell<T>
is a type that provides simple, byte-wise copy-able mutability. It is commonly used for types that implement the Copy
trait, like integers and booleans. Cell<T>
is used when you need to mutate a value without using a reference or a mutable reference. Common methods include set
, get
, and replace
⮳ replace
⮳ .
use std::cell::Cell; struct MyStruct<T> { // The `Cell` type allows for interior mutability. value: Cell<T>, } impl<T> MyStruct<T> { fn new(value: T) -> Self { MyStruct { value: Cell::new(value), } } // Replace the interior value, dropping the replaced value. fn set_value(&self, new_value: T) { self.value.set(new_value); } } impl<T: Copy> MyStruct<T> { fn get_value(&self) -> T { // For types that implement `Copy`, the `get` method // retrieves the current interior value by duplicating it. self.value.get() } } // For types that implement `Default`, the take method replaces the current // interior value with `Default::default()` and returns the replaced value. impl<T: Default> MyStruct<T> { fn take_value(&self) -> T { self.value.take() } } fn main() { let my_int_struct = MyStruct::new(42); let my_int_ref = &my_int_struct; // Get the current value (`i32` is `Copy`). println!("Initial value: {}", my_int_struct.get_value()); // Set a new value, while using an immutable reference to the struct. my_int_ref.set_value(100); // Get the updated value. println!("Updated value: {}", my_int_ref.get_value()); let my_string_struct = MyStruct::new("example".to_string()); let my_string_ref = &my_string_struct; // ERROR, `String` is not `Copy`: `my_string.get_value();` // Get the value, this time using the `take_value` method. println!("String: {}", my_string_ref.take_value()); assert_eq!(my_string_ref.take_value(), ""); // Replace the current interior value and returns the replaced value. assert_eq!(my_string_struct.value.replace("example2".into()), ""); // `into_inner` consumes the `Cell<T>` and returns the interior value. let _inner = my_string_struct.value.into_inner(); }
"Cell<T>
implements interior mutability by moving values in and out of the cell. That is, an &mut T
to the inner value can never be obtained, and the value itself cannot be directly obtained without replacing it with something else."
OnceCell
- The
OnceCell
type provides a way to define a value that will be initialized at most once. It's useful for scenarios where you want lazy initialization without the overhead of thread synchronization.
Related Data Structures
- COW (Copy-on-Write).
Related Topics
- Concurrency.
- Memory Management.
- Memory Usage Analysis.
- Rust Patterns.
- Shared State.