Smart Pointers In Rust
image source - https://www.technotification.com/2018/09/learn-rust-programming-language.html

Smart Pointers In Rust

Smart pointers are a fairly advanced topic in Rust, but we might have used them without even noticing it. For example, Rust's Vector and String data types use smart pointers to manage their data efficiently. The structure is such that the actual data is stored in a heap, which is pointed by a pointer(smart pointer) stored in a stack. You can dive deep into stacks and heaps in Rust's Memory Management.

Is Smart Pointers exclusive to Rust? No, smart pointers originated in C++ and then made their way to some of the other programming languages. But we will learn about smart pointers in the context of our favourite Rusty ??. We will learn about the smart pointers commonly used in Rust (std library) and why we need them. We will try to figure out problems that can be faced when working with dynamic memory and solve them using smart pointers.

Smart pointers we are going to cover:

  1. Box<T>
  2. Rc<T>

Smart Pointer - Box<T>

We know that variables of primitive type are stored in the stack since these variables have fixed sizes, and some primitive types have copy traits like Integers, Floats, Boolean, and Char implement Copy traits by default.

fn main() {
  let first_number: i32 = 20;
  let second_number: i32 = 30;

  // copy of first_number and second_number
  print_sum(first_number, second_number);
}

fn print_sum(x: i32, y: i32) {
  println!("The sum is {}", x + y);
}        

What if we want to store such data in a Heap memory and ensure that the actual data is not copied around? For such occasions, You can use the Box Pointer.

This is not popular, and you may not use it in your projects, but let's assume that you want to hold a number that you want to decide to store in a heap and ensure that the value is not being copied to multiple sections of your code. Instead, you want a pointer pointing to its heap memory to be copied (which is tiny compared to the actual data).

// store the value(i32) in heap memory using Box 
let large_number: Box<i32> = Box::new(5000);        

Although the above step may seem justifiable(in some cases), storing every value in a heap is not recommended. The example is to let you know the power of the Box pointer and how it can be used(even for unusual situations like shown above). But there is another use case of the Box pointer, which is used popularly.

Enabling Recursive Types with Boxes

Let's understand Recursive Types first. A value of recursive type can have another value of the same type as part of itself. Confused? You don't need to worry. Consider the example of a struct - Node:

    struct Node {
        value: i32,
        next: Node,
    }        

The above snippet shows a struct Node where the next is of type Node, which is what recursive type looks like. What do you think the compiler will show when you write this code? If you said it would throw a compiler error, you deserve a pat on your back(really, do it!). This is the compiler error that will be shown:

recursive type Node has infinite size recursive type has infinite size

Why this error?

This has to do with the Rust compiler, as it needs to know the size of every variable in the compile time. Since the next value has a type Node, which again can contain another Node and so on, the compiler thinks that this "loop" can run till infinity before coming to an end. Also, the compiler lets you know about the possible solutions you can do. One of them is what we are going to look at next.

Solution - Wrap with Box

Since Box has a fixed size, it can be used to wrap the Node (in our example) to comfort our compiler and complete our program.

    struct Node {
        value: i32,
        next: Box<Node>,
    }        

The above code is how you handle variables with recursive type. Wrapping the Box<Node> with Option enum as a node can also be Nil(not provided) or None is also advisable. You can check the completed solution using Option Enum in my replit.

Rc<T>

What if we want data to be shared among multiple owners? This violates Rust's ownership model. Yes, I agree. But what about having multiple references for multiple owners? Let's understand this with an example:

    1 let mut first_number: i32 = 20;
    2 let second_number: &i32 = &first_number;
    3
    4 first_number = 20;
    5 dbg!(second_number);        

Although the above code looks harmless, it raises a compiler issue

cannot assign to first_number because it is borrowed assignment to borrowed first_number occurs here

Why this error?

The compiler knows that the reference of first_number borrowed by the second_number has not returned the ownership to first_number. It means the memory of first_number is locked until second_number operations are complete. In this case, the second_number's operation is said to be completed once it has printed the second_number(5th line).

This is not an issue, but this is one of the many reasons why Rust is known for. It deliberately locks the file until the borrower of its ownership has completed its job. So, how can we manage such a situation where we want multiple references and still not produce any error while maintaining the ownership model?

Solution - Reference Counted Smart Pointer (Rc)

Rc manages this situation in a very intuitive way. It tracks all the references for a value stored in a heap and deallocates the memory once the last reference goes out of scope. This way, the compiler is assured that the data will persist until at least one reference is used somewhere, allowing the data to have multiple ownership with shared references.

// initialize a Reference Counted Smart pointer
let first_number: Rc<i32> = Rc::new(50);

// clone the first_number reference
let second_number: Rc<i32> = Rc::clone(&first_number);        

So that you know, Rc makes an immutable reference. We will see how to complete our program by mutating the first_number.

RefCell<T>

RefCell is not a smart pointer but a type of interior mutability mechanism. This means we can mutate an immutable reference. Sounds strange, right? But let's see how it can solve our problem.

First, let's discuss the problems that can be faced while mutating with Rc.

fn main() {
    let mut first_number: Rc<i32> = Rc::new(50);
    let second_number: Rc<i32> = Rc::clone(&first_number);

    first_number = Rc::new(30);

    dbg!(first_number);
    dbg!(second_number);
}

output:
[src/main.rs:9] first_number = 30
[src/main.rs:10] second_number = 50        

We have achieved the mutation of first_number by assigning a new Rc (Rc::new(30)) value. Since Rc is immutable, this solution makes sense, but look closely at the output where now the first_number and second_number are not the same. The second_number holds the reference of the previous value of first_number.

In real situations, the expectation would be to update all the values with the value it refers to. So, in our example, the second_number is expected to be 30. This is where RefCell comes into the picture.

Using RefCell To Mutate Rc

use std::{cell::RefCell, rc::Rc};

fn main() {
    let first_number: Rc<RefCell<i32>> = Rc::new(RefCell::new(50));
    let second_number: Rc<RefCell<i32>> = Rc::clone(&first_number);

    *first_number.borrow_mut() = 30;

    dbg!(first_number);
    dbg!(second_number);
}

output:
[src/main.rs:9] first_number = RefCell { value: 30 }
[src/main.rs:10] second_number = RefCell { value: 30 }        

Rust is very conservative about the borrowing rules in the compile time. These are the borrowing rules of Rust that the compiler checks (for us):

  1. At any given time, you can have either (but not both) one mutable reference or any number of immutable references.
  2. References must always be valid.

So even though in some cases we, as a developer, know that there won't be a borrowing issue with our code, the compiler wants to take risks and might not allow you to run the code. To mutate an immutable reference, we must utilise the concept of interior mutability, which uses unsafe blocks under the hood. One of the standard libraries that use interior mutability out of the box is our friendly RefCell.

The magic that happens under the hood of interior mutability(unsafe) is a topic for an entirely new article. Still, it enforces the borrow check of the data in the runtime. It is similar to a manual check because you take responsibility for checking borrow rules and ensure rusty doesn't panic in the runtime as you disable the compiler's borrow check for the data reference used in RefCell.

Let's first take an example of RefCell:

use std::cell::RefCell;

fn main() {
    let first_number: RefCell<i32> = RefCell::new(50);
    *first_number.borrow_mut() = 20;

    dbg!(first_number.borrow());
    dbg!(first_number);
}

output:
[src/main.rs:7] first_number.borrow() = 20
[src/main.rs:8] first_number = RefCell {
    value: 20,
}
        

Notice that we can mutate the reference of first_number even though first_number is not declared mutable. This is RefCell in action. Two methods of RefCell are used here. One is .borrow_mut(), and the other is .borrow(). .borrow_mut() is used to get a mutable reference, and .borrow() is used to get a shared immutable reference.

Since RefCell manages data as a reference, we need to deref first_number to call the .borrow_mut() method, and because first_number is wrapped in a RefCell block, we need to use .borrow() to get the value of first_number to be printed. RefCell is fantastic, but with great power comes great responsibility, so check for any borrow-related blockers to avoid rusty to panic because you are dealing with RefCell in runtime.

Piecing It Altogether:

Now that we have understood Rc and RefCell. Let's combine it and complete our program.

use std::{cell::RefCell, rc::Rc};

fn main() {
    let first_number: Rc<RefCell<i32>> = Rc::new(RefCell::new(50));
    let second_number: Rc<RefCell<i32>> = Rc::clone(&first_number);

    *first_number.borrow_mut() = 30;

    dbg!(first_number.borrow());
    dbg!(second_number.borrow());
}

output:
[src/main.rs:9] first_number.borrow() = 30
[src/main.rs:10] second_number.borrow() = 30        

I hope you enjoyed the read and learned something new. I have added all the coding examples in this article to my replit.

You can take a look at my other articles on Rust ??:

  1. Ownership Model.
  2. Memory Management.
  3. Rust Terminologies (Beginner edition).

要查看或添加评论,请登录

Mukesh kumar的更多文章

  • Generic Types in Rust

    Generic Types in Rust

    Generic types in Rust are exciting topics to learn, which may be intimidating for someone new to strongly typed…

    3 条评论
  • Concurrency in Rust ??

    Concurrency in Rust ??

    Hi Rustaceans, welcome to another article on Rust. In this article, we will learn about achieving concurrency.

  • Rust Terminologies (Beginner Edition)

    Rust Terminologies (Beginner Edition)

    Learning something new and coming across specific technical terms that slow your journey always feels frustrating…

  • Memory management in Rust

    Memory management in Rust

    Memory management is one of the many things that are done by the program automatically, and most often, we need to…

    2 条评论
  • Ownership Model of Rust

    Ownership Model of Rust

    Rust has indeed taken the best of two worlds that are of having low-level control like C and C++ (that are 45 years…

    5 条评论
  • Deployed my first "Hello DApp" (Decentralised App) ?? .

    Deployed my first "Hello DApp" (Decentralised App) ?? .

    What is a Decentralised App(DApp) ?? ? DApp is an application running on a decentralized peer-to-peer network. Although…

社区洞察

其他会员也浏览了