Open In App

Rust – Concept of Smart Pointers

Last Updated : 21 Jul, 2021
Improve
Improve
Like Article
Like
Save
Share
Report

Pointers are basically variables that store the address of another variable.  While smart pointers are data types that behave like a traditional pointer with additional capabilities such as automatic memory management or bounds checking.

What makes smart pointers different from normal pointers are:-

  • They are structs and contain additional metadata and capabilities.
  • They do not borrow the data rather they own it.

What makes smart pointers different from structs:-

  • They implement the Deref and Drop traits.

Deref trait allows an instance of a smart pointer struct to behave like a reference so that the code that works with pointers can also work with smart pointers.

Drop trait allows us to customize the code that should run when an instance of the smart pointer goes out of scope.

Some of the smart pointers are:-

  • Box<T> to allocate values on the heap
  • Rc<T> a reference counting type that enables multiple ownership

Box<T>

Box allows us to store data in the heap contrary to the default scheme of Rust to store as a stack.

Box is basically used:

  • For dynamic allocation of memory for variables.
  • When there is a lot of data that we need to transfer ownership, and we don’t want them to are copied.

Let’s create a box to store i32 value in a heap

Rust




fn main() {
    let num = Box::new(4);
    println!("num = {}", num);
}


Output

num = 4

Using Box<T> for recursive type

We will be using cons list to create a list of values. cons list takes to values the first one is the current value and the other is the next value, it performs a recursive call to cons function to generate a list, where the base condition for the recursive call is Nil.

Rust




enum List {
    Cons(i32, List),
    Nil,
}
use crate::List::{Cons, Nil};
  
fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}


It won’t compile because the List variable size cannot be determined prior to compilation.

Output:

 rustc -o main main.rs
error[E0072]: recursive type `List` has infinite size
 --> main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
  = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable

error[E0391]: cycle detected when processing `List`
 --> main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which again requires processing `List`, completing the cycle
  = note: cycle used when computing dropck types for `Canonical { max_universe: U0, variables: [], value: ParamEnvAnd { param_env: ParamEnv { caller_bounds: [], reveal: UserFacing, def_id: None }, value: List } }`

There is an error that tells the List has infinite size because the compiler cannot determine the size of List during compilation. So we will be using a pointer to the list rather than the list itself to overcome this error. Since the size of the pointer is fixed irrespective of the data type to which it is pointing to, therefore the compiler can determine its size during compilation. Let’s see this implementation using Box<T>

Rust




#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}
  
use crate::List::{Cons, Nil};
  
fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("{:?}",list)
}


Output:

Cons(1, Cons(2, Cons(3, Nil)))

Defining our own smart pointer:

Rust




struct CustomPointer<T>(T);
  
impl<T> CustomPointer<T> {
    fn new(x: T) -> CustomPointer<T> {
        CustomPointer(x)
    }
}


Here, we have defined a Custom smart pointer as a tuple struct with one value. We have defined a new method which is called when the smart pointer is instantiated, that returns a custom smart pointer.

Using Dereference trait on our custom smart pointer:

Just like an ordinary pointer, we can get the value of the box by using the ‘*’ operator.

Rust




struct CustomPointer<T>(T);
  
impl<T> CustomPointer<T> {
    fn new(x: T) -> CustomPointer<T> {
        CustomPointer(x)
    }
}
//implementing deref trait
use std::ops::Deref;
impl<T> Deref for CustomPointer<T> {
    type Target = T;
      
    fn deref(&self) -> &Self::Target {
        &self.0
    }
      
}
fn main() {
    let x = 5;
    let y = CustomPointer::new(x);
  
    println!("{}",x==5);
    println!("{}",*y==5);
}


Output:

true
true

Using Drop trait on our smart pointer:

Whenever the instance of smart pointer goes out of scope then the drop function is called.

Rust




// defining custom smart pointer
struct CustomPointer <T>(T);
  
impl<T> CustomPointer<T> {
    fn new(x: T) -> CustomPointer<T> {
        CustomPointer(x)
    }
}
use std::ops::Deref;
  
// for implementing deref trait
impl<T> Deref for CustomPointer<T> {
    type Target = T;
      
    fn deref(&self) -> &Self::Target {
        &self.0
    }
      
}
// for implementing drop trait
impl <T> Drop for CustomPointer<T> {
    fn drop(&mut self) {
        println!("Dropping CustomtPointer with data");
    }
}
fn main() {
    let x = 5;
    let y = CustomPointer::new(x);
  
    println!("{}",x==5);
    println!("{}",*y==5);
}


Output:

true
true
Dropping CustomtPointer with data


Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads