What are Smart Pointers in RUST?

In Rust, Smart Pointers are not only pointers but a data structure as well. They are also available in many other languages, but their origin lies in c++. Smart pointers are generally implemented through structs.

Following are the types of Smart Pointer in rust.

1. Box

The box is the most simple smart pointer which enables recursive types. At compile time rust compiler wants to know how much space will be required by a type, and that becomes difficult in recursion as theoretically, it can be infinite. So, we use box type to provide the size of the type so that compiler knows how much memory is to be allocated.

enum List {
   Cons(i32, Box),
   Nil,
}

use List::{Cons, Nil};

fn main() {
let list = Cons(1,
                  Box::new(Cons(2,
                                Box::new(Cons(3,
                                              Box::new(Nil))))));
}

In the above example, the Cons variant needs the size of i32 plus space to store the box pointer data. By using the box we have broken the infinite recursive chain and compiler can now figure out the size of List.

2. Deref Trait

The Deref trait allows us to customize the behavior of dereferencing an operator. In the below example, we define our own smart pointer implementation by creating struct name MyBox and declare a generic parameter T as we want to hold values of any type

use std::ops::Deref;

struct MyBox(T);

impl MyBox {
    fn new(x: T) -> MyBox {
        MyBox(x)
    }
}

impl Deref for MyBox {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MyBox::new(String::from("World"));
    hello(&(*m)[..];
}

Here we created our box type smart pointer using Deref trait. the (*m) dereferences the MyBox into a string then & and [..]take a slice of a string.

3.Drop trait

The drop trait is an important smart pointer which provides us the ability to customize what happens with the value after it goes out of scope and you can also provide the definition to drop trait.

Filename: src/main.rs

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let a = CustomSmartPointer { data: String::from("first variable") };
    let b = CustomSmartPointer { data: String::from("second variable") };
    println!("CustomSmartPointers created.");
}

Output:

CustomSmartPointers created.
Dropping CustomSmartPointer with data `second variable`!
Dropping CustomSmartPointer with data `first variable`!

Here we do not need to call drop function when the main finished and variables go out of the scope, the drop method is automatically called. Variables are always dropped in the reverse order of creation so first b is dropped and after that a is dropped.

If you want to drop it early then you should use std::mem::drop method to drop variable early.

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer!");
    }
}

fn main() {
    let p = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(p);
    println!("CustomSmartPointer dropped before the end of main.");
}

Output:

CustomSmartPointer created.
Dropping CustomSmartPointer!
CustomSmartPointer dropped before the end of main.

4. Reference Counted

Reference Counted smart pointer is used to enable multiple ownership. Rust has a type RC which keeps the track of the number of references to a value from which we can know about how many places our variable is used. If the reference count is zero then the value is not used anywhere so the value can be cleaned up safely without worrying about any references becoming invalid.

enum List {
    Cons(i32, Rc),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

Output:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Here we can see that RC count is one when we create variable a, after that, when we create another variable b by cloning variable a, the count goes up by 1 to 2.  Similarly, when we create another variable c by again cloning variable a, the count further goes up by 1 to 3. After c goes out of scope count goes down by 1 to 2.

5. Interior Mutability Pattern

Interior mutability pattern in rust allows us to mutate data even when there are immutable references to the data. Normally borrowing rules do not allow us to perform such operations, so to achieve this rust provide us the type Refcell<T>  which can let us mutate data even when there are immutable references. A common way to use Refcell<T> is in combination with Rc<T> which is a reference counter. If we have multiple owners of some data and we want to give access to mutate data then we have to use Rc<T> that hold a Refcell<T>.

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell>, Rc),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
}

Output:

a after = Cons(RefCell { value: 15 }, Nil)

In the above example, we have created an instance of Rc<Refcell<i32>> and store it in a variable named value and also we created a list a with a cons variant that holds the value. We cloned value so that both a and value have ownership of inner cell which has a value of 5 rather than transferring ownership from value. After that, we add 10 in value by calling borrow_mut() on value and this method return RefMut<T> smart pointer and we use reference operator on it to change the inner value of it.

Written by 

Rahul Singh Bhati is the Trainee Software Consultant at Knoldus Software LLP. He has done B.Tech. from Jaypee Institute of Information Technology, Noida(Sec-62). He has good knowledge of languages C, C++, Java, Scala, HTML, CSS, PHP, Python, and Rust. He is also working on many frameworks like lagom, play and actix. As a fresher, he always tries to explore the different type of software and tools.

Leave a Reply

%d bloggers like this: