Chapter 15: Smart Pointers
Smart Pointers
A smart pointer is a pointer with additional metadata and capabilities, such as counting reference to an object.
Typically, a smart pointer is a struct that implements two traits:
Deref
: allow the smart pointer to behave like a reference. Making functions that takes reference also takes smart pointer.Drop
: properly handle the pointed value when the pointer goes out of scope
Box<T>
A box is a pointer on stack that points to some data on heap.
It’s similar to unique_ptr
in CPP.
When a box goes out of scope, both the pointer on stack and the data it points to on heap will be deallocated.
Basic Usage
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
Recursive Types
There are types who contains itself, such as a Node pointing to another node. They are called recursive types. The size of these types is unknown at compile time.
Rust needs to know the size of a type at compile time. As a solution, we store a pointer to the type in the type.
An example would be a node in a singly linked list.
The most intuitive way to define a node won’t work
struct Node<T> {
value: T,
next: Option<Node<T>>
}
This gives error recursive type
Nodehas infinite size
. Instead, we can use a box to hold the next node.
// A node in singly linked list.
struct Node<T> {
value: T,
// either "null", or pointer to next node
next: Option<Box<Node<T>>>,
}
Below is a struct of singly linked list based on this Node
type.
pub struct SLL<T> {
head: Option<Box<Node<T>>>,
length: usize,
}
impl<T: Default + Debug> SLL<T> {
pub fn new() -> SLL<T> {
SLL {
head: None,
length: 0
}
}
pub fn push_front(&mut self, value: T) {
let new_head = Box::new(Node {
value,
next: self.head.take(),
});
self.head = Some(new_head);
self.length += 1;
}
pub fn pop_front(&mut self) -> Option<T> {
self.head.take().map(|node| {
self.head = node.next;
self.length -= 1;
node.value
})
}
pub fn print_list(&self) {
// cur_opt is a mutable variable that immutably borrows an Option<Box<Node<T>>>
let mut cur_opt = &self.head;
while let Some(cur_node) = cur_opt {
print!("{:?} -> ", cur_node.value);
cur_opt = &cur_node.next;
}
println!("None");
}
}
#[allow(dead_code)]
fn recursive_type_demo() {
let mut list = SLL::new();
list.push_front(3);
list.push_front(4);
list.push_front(5);
list.push_front(6);
list.push_front(7);
list.print_list();
list.pop_front();
list.pop_front();
list.print_list();
}
Side note: similar problem occurs in cpp, but with a different error report
struct Node {
int value;
Node next;
};
The Node
above gives this error
sll.cpp:10:7: error: field has incomplete type 'Node'
Node next;
^
sll.cpp:8:8: note: definition of 'Node' is not complete until the closing '}'
struct Node {
^
1 error generated.
Deref
The trait Deref
has an associated type called Target
. The implementor of Deref
will return a &Target
when calling deref()
.
pub trait Deref {
type Target: ?Sized;
// Required method
fn deref(&self) -> &Self::Target;
}
Typically, we would define a way to dereference a custom type like this
struct MyStruct {val: i32}
impl Deref for MyStruct {
type Target = i32;
fn deref(&self) -> &Self::Target {
&self.val
}
}
#[allow(dead_code)]
fn deref_demo() {
let val = 31;
let my_var = MyStruct { val };
assert_eq!(val, *my_var);
assert_eq!(val, *my_var.deref());
}
Note that deref
returns a reference to type Target
, instead of the actual Target
value. This is due to the ownership system of Rust: if Target
value is returned, it would be moved out of self
. Deref
is typically implemeneted for smart pointer, and we don’t want to tranfer ownership when we deref a pointer.
Deref Coersion
When type A implements Deref
trait with Target
type B
, we can pass a variable of type &A
to a function that takes &B
, and Rust will automatically calls A.deref()
to convert A
to &B
.
// Deref coersion that convert type `A` to type `&B`
struct A {val: B}
struct B {}
impl Deref for A {
type Target = B;
fn deref(&self) -> &Self::Target {
println!("A -> &B");
&self.val
}
}
fn accept_b(_val: &B) {
println!("Accepting a parameter of type &B.")
}
fn abs_deref_coersion_demo() {
let b = B {};
let a = A {val: b};
accept_b(&a);
println!("Manually calling A.deref()");
let arb = a.deref();
accept_b(arb);
}
The output is
## Deref Coersion
A -> &B
Accepting a parameter of type &B.
## Manually ref and deref w/ & and *
A -> &B
Accepting a parameter of type &B.
## Manually calling deref()
Manually calling A.deref()
A -> &B
Accepting a parameter of type &B.
In other words, if A
implements Deref
with Target=B
, Rust will find a way to do implicit type conversion from &A
to &B
. Logically, we can explicitly write out this implicit conversion as &*(A.deref())
.
A useful example is implicitly convert &String
to &str
. String
implements Deref
with Target = str
, meaing we can pass &String
to functions that takes &str
as parameter. The implementation is actually quite short
#[stable(feature = "rust1", since = "1.0.0")]
impl ops::Deref for String {
type Target = str;
#[inline]
fn deref(&self) -> &str {
self.as_str()
}
}
To take one step further, you can even chain deref coersion together, like this
// Chain of deref coersion that convert type `D` to type `&F`
struct D {val: E}
struct E {val: F}
struct F {}
impl Deref for D {
type Target = E;
fn deref(&self) -> &Self::Target {
println!("D -> &E");
&self.val
}
}
impl Deref for E {
type Target = F;
fn deref(&self) -> &Self::Target {
println!("E -> &F");
&self.val
}
}
fn accept_e(_val: &E) {
println!("Accepting a parameter of type &E.")
}
fn accept_f(_val: &F) {
println!("Accepting a parameter of type &F.")
}
#[allow(dead_code)]
fn abs_chain_deref_coersion_demo() {
let f = F {};
let e = E {val: f};
let d = D {val: e};
println!("## Deref Coersion");
accept_e(&d);
accept_f(&d);
println!("## Manually ref and deref w/ & and *");
accept_e(&*d);
accept_f(&*&*d);
println!("## Manually calling deref()");
println!("Manually calling A.deref()");
let dre = d.deref();
accept_e(dre);
println!("Manually calling B.deref()");
let erf = (*dre).deref();
accept_f(erf);
}
The output is
## Deref Coersion
D -> &E
Accepting a parameter of type &E.
D -> &E
E -> &F
Accepting a parameter of type &F.
## Manually ref and deref w/ & and *
D -> &E
Accepting a parameter of type &E.
D -> &E
E -> &F
Accepting a parameter of type &F.
## Manually calling deref()
Manually calling A.deref()
D -> &E
Accepting a parameter of type &E.
Manually calling B.deref()
E -> &F
Accepting a parameter of type &F.
Deref coersion happens at compile time, so no penalty at runtime for using this feature!
Mutability of Deref Coersion
Deref coersion is possible when converting
&T
to&U
whereT: Deref<Target=U>
&mut T
to&mut U
whereT: DerefMut<Target=U>
&mut T
to&U
whereT: Deref<Target=U>
Note that &T
to &mut U
is impossible, b/c there could be multiple immutable reference to T
when doing the conversion. If this is allowed, we would end up with one mutable ref and potentially multiple immutable ref to the same T
, which violate Rust’s borrowing rules.
For the same reason, the third conversion is possible, b/c we would go from “only one mutable ref” to “only one immutable ref”.
Drop
Trait Drop
allows us to define a function Drop::drop()
that customizes what would happen when a variable goes out of scope.
For variables within the same scope, as they goes out of scope, they are dropped in the reverse order of their definition, i.e. LIFO.
Early Drop
You might want to call drop
before the variable goes out of scope. For example, a smart pointer holding a lock may need to be dropped early so that other variables can obtain the lock. Think about PageGuard in BusTub, and how they work when implementing B+Tree.
Manually calling Drop::drop()
is not allowed, o/w there would be a double-free issue. As an alternative, we can use std::mem::drop
and pass in the variable to-be-dropped, like std::mem::drop(my_ptr)
.
To quote from the doc: This function is not magic; it is literally defined as
pub fn drop<T>(_x: T) {}
It moves the argument into the function, thus the argument is automatically dropped before the function returns.
A slightly unexpected result of this implementation is that passing in any argument that implements Copy
trait, such as i32
, will take no effect. This is because the argument will be copied into the std::mem::drop
function.
Reference-counting Pointer Rc<T>
The type Rc<T>
in std::rc
is a reference-counting pointer. It allows shared ownership of a value of type T
, allocated in the heap. Similar construct in cpp is std::shared_ptr<T>
.
The typical usage of Rc<T>
is to
create a value on stack
create the first
Rc<T>
usingRc::new()
. This will initialize aRc<T>
by copying the value to heap.create more
Rc<T>
usingRc::clone()
. TheseRc<T>
will refer to the same value allocated on heap.
use std::rc::Rc;
let five = Rc::new(5);
let _ = Rc::clone(&five);
Internally,
When
Rc::new()
is called, ref counter is set to 1When
Rc::clone()
is called, ref counter increment by 1When the
Rc<T>
goes out of scope,std::ops::Drop::drop()
is called automatically, which decrement ref counter by 1. If the ref counter reaches 0, the value on heap is deallocated automatically.
Note that the convention is to use
Rc::clone(&value)
instead ofvalue.clone()
. This is because mostClone
implementation will do a deep copy, whileRc
’s implementation only do shallow copy. Writing the code this way helps emphasize this point.
To get the number of strong referneces, we can call
Rc::strong_count(&a);
Interior Mutability and RefCell<T>
Rc<T>
provides multiple owning immutable reference to the heap value. Sometimes, we want have multiple owning mutable reference. For example, in a DAG, we can write our code s.t. a node is owned by all its neighbors. It would be convenient to modify the node value as we traverse the graph. RefCell<T>
provides this feature: we can define an immutable variable of type RefCell<T>
that points to a value of type T
on heap, and modify the value using RefCell::borrow_mut()
. This pattern is called Interior Mutability:
- Interior Mutability: we hold an immutable reference of an abstraction of a value (
RefCell<T>
), and mutate the interior value (T
) through this immutable abstraction.
As an example, we can define a list where both the node value and next node pointer is interiorly mutable.
#[derive(Debug)]
pub enum List {
Cons(Rc<RefCell<String>>, RefCell<Rc<List>>),
Nil
}
mod tests {
use std::ops::Deref;
use super::*;
fn mut_val() {
println!("## Test mutable value in list node");
let value = Rc::new(RefCell::new("AAA".to_string()));
let a = Rc::new(List::Cons(Rc::clone(&value), RefCell::new(Rc::new(List::Nil))));
println!("a: {a:?}");
*value.borrow_mut() = "BBB".to_string();
println!("a: {a:?}");
if let List::Cons(ref_val, _) = Rc::deref(&a) {
// We have two immutable reference to one i32 value
// that allows us to mutate the value
*ref_val.borrow_mut() = "CCC".to_string();
println!("a: {a:?}");
};
println!("a: {a:?}");
println!();
}
#[test]
fn mut_next() {
println!("## Test mutable next node in list node");
let a1 = Rc::new(List::Cons(Rc::new(RefCell::new("A1".to_string())), RefCell::new(Rc::new(List::Nil))));
let a2 = Rc::new(List::Cons(Rc::new(RefCell::new("A2".to_string())), RefCell::new(Rc::new(List::Nil))));
let b = Rc::new(List::Cons(Rc::new(RefCell::new("B".to_string())), RefCell::new(Rc::clone(&a1))));
// Initially, b -> a1
println!("List: {:?}", b);
// We can change it to b -> a2
if let List::Cons(_, ref_next) = Rc::deref(&b) {
*ref_next.borrow_mut() = Rc::clone(&a2);
}
println!("List: {:?}", b);
println!();
}
}
Here, we have two usage of RefCell
, mixed with RC
. Let’s see how they work
- Node value:
Rc<RefCell<String>>
Rc
allows us to create multiple immutable reference to the underlying RefCell<String>
, while RefCell
allows each of these reference to modify the underlying String
.
Note that the multiple reference can’t modify RefCell<String>
. They can only modify the String
holded by RefCell
.
- Next node:
RefCell<Rc<List>>
RefCell
allows the current node to change the next node to point to. The next node itself can also be pointed by multiple nodes, thanks to Rc
.
Memory Leak and Weak<T>
Such a reference-counting pointer can lead to cyclic reference like this
#[derive(Debug)]
pub enum List {
Cons(Rc<RefCell<String>>, RefCell<Rc<List>>),
Nil
}
mod tests {
use std::ops::Deref;
use super::*;
#[test]
fn test_cyclic_ref() {
// ####### block 1
let a = Rc::new(List::Cons(Rc::new(RefCell::new("A".to_string())), RefCell::new(Rc::new(List::Nil))));
let b = Rc::new(List::Cons(Rc::new(RefCell::new("B".to_string())), RefCell::new(Rc::clone(&a))));
// b -> a
println!("List: {:?}", b);
// ####### block 2
// link a to b, causing cyclic ref
if let List::Cons(_, ref_next) = Rc::deref(&a) {
*ref_next.borrow_mut() = Rc::clone(&b);
}
// ####### block 3
// uncomment this line to cause a stack overflow runtime error
// b -> a -> b,
// - compile successfully
// - but cause stack overflow at runtime
// println!("List: {:?}", b);
}
}
In this example, A points to B, and B points to A. This creates a cyclic reference. Technically, if we keep record of the referrer of the heap value in Rc<T>
, and we are able to check if the referrer is in scope, cyclic reference won’t cause any memory leak: when dropping, we simply check if all referrers are valid to decide if we should deallocate value on heap. However, Rc<T>
only count the number of reference. This means, by the end of block 2, both A and B have ref count of 2. When both are dropped, the ref count only goes to 1. This leads to memory leak.
Memory leak doesn’t stop the program from compiling. Rust compiler is not able to check this kind of error, because the ref count is managed at runtime. If we uncomment block 3, we will get a stack overflow runtime error.
To avoid this problem, we can use the smart pointer Weak<T>
. It’s similar to Rc<T>
, except the reference to heap value is non-owning. Internally, Rc<T>
keep track of two kinds of counter
strong count: number of owning reference (
Rc<T>
) to the heap valueweak count: number of non-owning reference (
Weak<T>
) to the heap value
the heap value is deallocated only if strong count goes to 0, even if weak count is above 0.
Rc<T>
and Weak<T>
are conceptually similar to hard link and soft link in file system:
when a file is deleted, its hard link counter decrement by 1
a file is not actually deleted unless #hard link goes to 0
when #hard link goes to 0, the file can be deleted even if #soft link is nonzero.
- in this case, the soft link becomes invalid
if hard link to directory is allowed, it would create a cycle in the directory tree, similar to how a cyclic reference can be created by
Rc<T>
To use Weak<T>
, we would
create a
Weak<T>
fromRc<T>
usingRc::downgrade(rc: &Rc<T>) -> Weak<T>
access the value in
Weak<T>
as anOption<Rc<T>>
usingWeak::upgrade(&self) -> Option<Rc<T>>
. The option isNone
if the value is deallocated, similar to how we would access an invalid soft link in file system.
As an example,
#[derive(Debug)]
pub enum ListWeakRef {
Cons(Rc<RefCell<String>>, RefCell<Weak<ListWeakRef>>),
Nil
}
mod tests {
use std::ops::Deref;
use super::*;
#[test]
fn test_weak_ref() {
let a = Rc::new(ListWeakRef::Cons(Rc::new(RefCell::new("A".to_string())), RefCell::new(Weak::new())));
let b = Rc::new(ListWeakRef::Cons(Rc::new(RefCell::new("B".to_string())), RefCell::new(Rc::downgrade(&a))));
// b -> a
println!("a: {:?}", a);
println!("b: {:?}", b);
// b -> a -> b
if let ListWeakRef::Cons(_, ref_next) = Rc::deref(&a) {
*ref_next.borrow_mut() = Rc::downgrade(&b);
}
println!("a: {:?}", a);
println!("b: {:?}", b);
if let ListWeakRef::Cons(_, ref_next) = Rc::deref(&a) {
let a_next = ref_next.borrow();
// since it's weak ref, no problem
println!("a.next = {:?}", a_next.upgrade());
}
println!();
}
}
The following will be printed
a: Cons(RefCell { value: "A" }, RefCell { value: (Weak) })
b: Cons(RefCell { value: "B" }, RefCell { value: (Weak) })
a: Cons(RefCell { value: "A" }, RefCell { value: (Weak) })
b: Cons(RefCell { value: "B" }, RefCell { value: (Weak) })
a.next = Some(Cons(RefCell { value: "B" }, RefCell { value: (Weak) }))
The actual content of Weak<T>
won’t be printed, since upgrade()
needs to be called to access that content.
To get the number of weak referneces, we can call
Rc::weak_count(&a);
Cow
and std::borrow
std::borrow::Cow
represents a copy-on-write pointer. It can holds either a borrowed value or an owned value. When it holds a borrowed value, and we modify the underlying value, Cow
will call the std::borrow::ToOwned::to_owned()
method of the borrowed value, which will creates owned data from borrowed data, usually by cloning.
This is the signature of Cow
pub enum Cow<'a, B>
where
B: 'a + ToOwned + ?Sized,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
It is an enum with two variants: Cow::Borrowed
and Cow::Owned
. When the internal value is borrowed, the Cow
enum is Borrowed
; when its owned, the Cow
enum is Owned
.
As an example, we can write a function that test if a cow pointer’s value is borrowed or owned:
fn is_borrowed_or_owned<B: ToOwned + ?Sized>(cow_ptr: &Cow<B>) {
match cow_ptr {
Cow::Borrowed(_) => {
println!("cow's value is borrowed");
}
Cow::Owned(_) => {
println!("cow's value is owned");
}
}
}
Below is an example where we create a page wrapped in Cow::Owned()
, and copy it using Cow::Borrowed()
fn cow_basic() {
let page: Cow<[i32]> = Cow::Owned(vec![0, 1, 2]);
let mut page_copy = Cow::Borrowed(&page[..]);
println!("cow's value: {:?}", page_copy);
is_borrowed_or_owned(&page_copy);
page_copy.to_mut()[0] = 33;
println!("cow's value: {:?}", page_copy);
is_borrowed_or_owned(&page_copy);
}
We can actually let the compiler decide if Cow
should be initialized as Owned
or Borrowed
by passing the value to Cow::from()
fn cow_from_basic() {
let page: Cow<[i32]> = Cow::from(vec![0, 1, 2]);
let mut page_copy = Cow::from(&page[..]);
println!("cow's value: {:?}", page_copy);
is_borrowed_or_owned(&page_copy);
page_copy.to_mut()[0] = 33;
println!("cow's value: {:?}", page_copy);
is_borrowed_or_owned(&page_copy);
}