Trusted answers to developer questions
Trusted Answers to Developer Questions

Related Tags

rust
communitycreator

What are smart pointers in Rust?

Shanzay Gauhar

Smart pointers are data structures that perform the same functionalities as a pointer and offer additional features like memory management. Another feature that smart pointers have that pointers don’t have is that they own the data. Smart pointers are used for resource management such as managing file handles and network connection. Both String and Vector(Vec<T>) are considered since they have memory and enable the user to manipulate it.

Structs are commonly used to implement smart pointers; however, smart pointers are not like ordinary structs as they offer a Deref and Drop trait.

There are many types of smart pointers:

In the above image the <T> next every smart pointer denotes the data type, i.e., <T> denotes the type of data.

Box<T>

In Rust, everything is allocated on stack by default. However, Box<T> is a smart pointer that provides a reference to the data on the heap rather than the stackcontains a pointer to the heap data. It does not incur any performance overhead apart from storing data on the heap. Box has a single owner of data. In correspondence to the borrowing rules, it can have one mutable reference or any number of immutable references, but it can’t have both simultaneously. This rules, along with the other borrowing rules in Box pointer, is checked at compile time.

The visual representation of Box smart pointer is shown below:

fn main(){
 let _example = Box::new(200);
}

Construct function

The construct function is a data structure used to construct a new pair from its two given arguments. These pairs, which consist of pairs, form a list. We can compute the size of recursive type using the Box pointer as demonstrated below:

#[derive(Debug)] 
enum List {  
    Cons(i32, Box<List>),  
    Nil,  
}  
use List::{Cons, Nil};  
fn main()  
{  
  let example = Cons(100,Box::new(Cons(200,Box::new(Nil))));
  print!("{:?}",example);
}

This is one example of how Box helps solve the issue of recursive variant or the infinite size problem. Usually, size is unknown to the rust compiler, but since the Box smart pointer size is known to the compiler, there is no compiler error, and the smart pointer is able to point to the list value stored on the heap.

Deref<T>

Deref<T> is a type of smart pointer that allows a user to manipulate and customize the behavior of the dereferencing operator (*). It allows the creation of an instance of smart pointer struct that can work with codes that work for both smart pointers and references. The following sample demonstrates a simple implementation of Deref:

use :: std::ops::Deref;  
struct MyBox<T>  
{  
  data : T,  
}  
impl<T> Deref for MyBox<T>  
{  
  type Target = T;  //declares generic type parameter
  fn deref(&self) ->&T  
  {  
    &self.data  
  }
}  
fn main()  
{  
  let example = MyBox{data : 10};  
  print!("{}",*(example.deref()));  
} 

Rc<T>

The reference-counted pointer Rc<T>, another type of smart pointer, allows multiple users to own or reference the data stored on the heap. It essentially keeps track of how many owners of data are there, i.e., how many times a certain data on heap has been referenced. When no owner remainsmeans that no reference exists, the data can be cleaned up. This type of pointer is only supported in a single-threaded process and runs into compile-time error if used otherwise. A sample use of Rc is displayed below:

#[derive(Debug)] 
enum List   
{  
  Cons(i32, Rc<List>),  
  Nil,  
}  
use List::{Cons,Nil};  
use std::rc::Rc;  
fn main()  
{  
  //Rather than taking ownership of List3, we clone Rc<T> list that currently holds List3. This increases the number of references.
  let list3 = Rc::new(Cons(34, Rc::new(Cons(2,Rc::new(Nil)))));  
  let list2 = Cons(10, Rc::clone(&list3));  
  let list1 = Cons(30,Rc::new(Cons(40, Rc::clone(&list3))));
  println!("{:?}",list3);
  println!("{:?}",list2);
  println!("{:?}",list1);
} 

RefCell<T>

RefCell<T> supports the interior mutability pattern, which enables the user to mutate data even when the data is immutable – it allows one mutable or many immutable borrows, but not both at a given instance. Immutable data refers to the data whose state is unmodifiable after its creation (unlike mutable data which can be modified even after its creation).

There is only one owner of data while using this type of smart pointer as, similar to Rc pointer, it does not support multithreading. Unlike Box, the borrowing rules are checked at runtime, which provides the added benefit when memory-safe scenarios, that might not be allowed by the compiler, are given a go-ahead.

The RefCell contains two methods:

  • borrow()
  • borrow_mut()

These methods both keep track of runtime borrows (both Ref<T> and RefMut<T>) and both (Ref and RefMut) implement Deref trait. RefCell increases the count of immutable borrows when borrow is called and decreases the count when Ref<T> goes out of scope. The function definition is:

pub fn borrow(&self)->Ref<T>
pub fn borrow_mut(&self) -> RefMut<T>;  

The code below shows different scenarios that are allowed:

use std::cell::RefCell;  
fn main()  
{  
  let data = RefCell::new(15);  
  1. Only one mutable borrow:
  let mutable_data = data.borrow_mut();     
}  
  1. Multiple immutable borrows:
 let immutable_data_1 = data.borrow();  
 let immutable_data_2 = data.borrow();
}  
  1. Incorrect - Both mutable and immutable borrows:
 let immutable_data_1 = data.borrow(); 
 let mutable_data = data.borrow_mut();  // creates panic.  

In order to have multiple owners of mutable data, use Rc and RefCell in conjunction.

Drop<T>

When a variable goes out of scope, Drop<T> is used to free the allocated space on the heap. The drop trait helps us customize what happens when a variable goes out of scope. The sample code structure below displays the implementation of the drop trait:

struct SampleSmartPointerImplementation {
    data: String,
}

impl Drop for SampleSmartPointerImplementation {
    fn drop(&mut self) {
        //Add your required sequence of actions here
    }
}

However, the drop method needs not be explicitly called because whenever a variable goes out of scope, the method is invoked itself. In Rust, the compiler will automatically execute the specified code when a value goes out of scope.

RELATED TAGS

rust
communitycreator
RELATED COURSES

View all Courses

Keep Exploring