Deborah Emeni I'm a software developer and technical writer who specializes in Node.js and JavaScript.

Understanding smart pointers in Rust

7 min read 2099

Understanding Smart Pointers In Rust

Developers can use conventional pointer methods when managing data on Heap or Stack. However, using these pointer methods comes with downsides, such as causing memory leaks when dynamically allocated objects are not garbage collected in time. The good news is that better memory management methods that automatically handle garbage collection with no runtime cost exist, and they are called smart pointers.

Rust is an open source, low-level, object-oriented, and statically typed programming language with efficient memory management that ensures high performance and security. It has a wide range of features that many organizations use to build highly secure and robust applications, including web, mobile, game, and networking apps.

This article will give you an understanding of what smart pointers are, their use cases, and their implementation in Rust. The included code example will teach you about Rust’s various types of smart pointers.

To jump ahead:

What are smart pointers?

Smart pointers are abstract data types that act like regular pointers (variables that store memory addresses of values) in programming, coupled with additional features like destructors and overloaded operators. They also include automatic memory management to tackle problems like memory leaks.

When a developer links memory that contains dynamically allocated data with smart pointers, they are automatically de-allocated or cleaned up.

Some smart pointer use cases include:

  • Automatically de-allocating data and destructing objects
  • Checking data or variables that exceed their bounds
  • Reducing bugs related to the use of regular pointers
  • Preserving the efficiency of the program after de-allocating data
  • Keeping track of all memory addresses of a program’s data/objects/variables
  • Managing network connections in a program application

How smart pointers work in Rust

Rust achieves memory management through a system (or a set of rules) called ownership, which is included in an application’s program and checked by the compiler before the program successfully compiles without causing any downtimes.

With the use of structs, Rust executes smart pointers. Among the additional capabilities of smart pointers previously mentioned, they also have the capability of possessing the value itself.

Next, you’ll learn about some traits that help customize the operation of smart pointers in Rust.



Deref trait

The Deref trait is used for effective dereferencing, enabling easy access to the data stored behind the smart pointers. You can use the Deref trait to treat smart pointers as a reference.

Dereferencing an operator implies using the unary operator * as a prefix to the memory address derived from a pointer with the unary reference operator & tagged “referencing.” The expression can either be mutable (&mut) or immutable (*mut). Using the dereferencing operator on the memory address returns the location of the value from the pointer points.

Therefore, the Deref trait simply customizes the behavior of the dereferencing operator.

Below is an illustration of the Deref trait:

fn main() {
        let first_data = 20;
        let second_data = &first_data;
        
        if first_data == *second_data {
                println!("The values are equal");
      } else {
             println!("The values are not equal");
     }
}

The function in the code block above implements the following:

  • Stores the value of 20 in a first_data variable
  • The second_data variable uses the reference operator & to store the memory address of the first_data variable
  • A condition that checks if the value of the first_data is equal to the value of the second_data. The dereferencing operator * is used on the second_data to get the value stored in the memory address of the pointer

The screenshot below shows the output of the code:

Code Output Of Deref Trait

Drop trait

The Drop trait is similar to the Deref trait but used for destructuring, which Rust automatically implements by cleaning up resources that are no longer being used by a program. So, the Drop trait is used on a pointer that stores the unused value, and then deallocates the space in memory that the value occupied.

To use the Drop trait, you’ll need to implement the drop() method with a mutable reference that executes destruction for values that are no longer needed or are out of scope, defined as:

fn drop(&mut self) {};

To get a better understanding of how the Drop trait works, see the following example:


More great articles from LogRocket:


struct Consensus  {
        small_town: i32
}

impl Drop for Consensus {
        fn drop(&mut self) {
                println!("This instance of Consensus has being dropped: {}", self.small_town);
}
}

fn main() {
        let _first_instance = Consensus{small_town: 10};
        let _second_instance = Consensus{small_town: 8};

        println!("Created instances of Consensus");
}
The code above implements the following:
  • A struct containing a value of a 32bit signed integer type called small_town is created
  • The Drop trait containing the drop() method with the mutable reference is implemented with the impl keyword on the struct. The message within the println! statement is printed to the console when the instances within the main() function go out of scope (that is, when the code within the main() function finishes running)
  • The main() function simply creates two instances of Consensusand prints the message within the println! to the screen once they are created

The screenshot below shows the output of the code: Output Code Of Drop Trait

Types of smart pointers in Rust and their use cases

Several types of smart pointers exist in Rust. In this section, you’ll learn about some of these types and their use cases with code examples. They include:

  • Rc<T>
  • Box<T>
  • RefCell<T>

The Rc<T> smart pointer

The Rc<T> stands for the Reference Counted smart pointer type. In Rust, each value has an owner per time, and it is against the ownership rules for a value to have multiple owners. However, when you declare a value and use it in multiple places in your code, the Reference Counted type allows you to create multiple references for your variable.

As the name implies, the Reference Counted smart pointer type keeps a record of the number of references you have for each variable in your code. When the count of the references returns zero, they are no longer in use, and the smart pointer cleans them up.

In the following example, you’ll be creating three lists that share ownership with one list. The first list will have two values, and the second and third lists will take the first list as their second values. This means that the last two lists will share ownership with the first list. You’ll start by including the Rc<T> prelude with the use statement, which will allow you to gain access to all the RC methods available to use in your code.

Then you will:

  • Define a list with the enum keyword and List{}
  • Create a pair of constructs with Cons() to hold a list of reference counted values
  • Declare another use statement for the defined list
  • Create a main function to implement the following:
    • Construct a new reference counted list as the first list
    • Create a second list by passing the reference of the first list as an argument. Use the clone() function, which creates a new pointer that points to the allocation of the values from the first list
    • Print the reference count after each list by calling the Rc::strong_count() function

Type the following code in your favorite code editor:

use std::rc::Rc;
 
enum List {
   Cons(i32, Rc<List>), Nil,
}
 
use List::{Cons, Nil};
 
fn main() {
   let _first_list = Rc::new(Cons(10, Rc::new(Cons(20, Rc::new(Nil)))));
   println!("The count after creating _first_list is {}", Rc::strong_count(&_first_list));
   let _second_list = Cons(8, Rc::clone(&_first_list));
   println!("The count after creating _second_list is {}", Rc::strong_count(&_first_list));
   { 
       let _third_list = Cons(9, Rc::clone(&_first_list));
       println!("The count after creating _third_list is {}", Rc::strong_count(&_first_list));
   }
 
   println!("The count after _third_list goes out of scope is {}", Rc::strong_count(&_first_list));
}

After you run the code, the result will be as follows:

Reference Counter Smart Pointer

The Box<T> smart pointer

In Rust, data allocation is usually done in a stack. However, some methods and types of smart pointers in Rust enable you to allocate your data in a heap. One of these types is the Box<T> smart pointer; the “<T>” represents the data type. To use the Box smart pointer to store a value in a heap, you can wrap this code: Box::new() around it. For example, say you’re storing a value in a heap:

fn main() {
        let stack_data = 20;
        let hp_data = Box::new(stack_data); // points to the data in the heap
        println!("hp_data = {}", hp_data);  // output will be 20.
}

From the code block above, note that:

  • The value stack_data is stored in a heap
  • The Box smart pointer hp_data is stored in the stack

In addition, you can easily dereference the data stored in a heap by using the asterisk (*) in front of hp_data. The output of the code will be:

Output Code Of Box Smart Pointer

The RefCell<T> smart pointer

RefCell<T> is a smart pointer type that executes the borrowing rules at runtime rather than at compile time. At compile time, developers in Rust may encounter an issue with the “borrow checker” where their code remains uncompiled due to not complying with the ownership rules of Rust.

Binding a variable with a value to another variable and using the second variable will create an error in Rust. The ownership rules in Rust ensure that each value has one owner. You cannot use a binding after its ownership has been moved because Rust creates a reference for every binding except with the use of the Copy trait.

The borrowing rules in Rust entail borrowing ownership as references where you can either have one/more references (&T) to a resource or one mutable reference (&mut T).

However, a design pattern in Rust called “interior mutability” allows you to mutate this data with immutable references. RefCell<T> uses this “interior mutability” design pattern with unsafe code in data and enforces the borrowing rules at runtime.

With RefCell<T>, both mutable and immutable borrows can be checked at runtime. So, if you have data with several immutable references in your code, with RefCell<T>, you can still mutate the data.

Previously, in the Rc<T> section, you used an example that implemented multiple shared ownerships. In the example below, you’ll modify the Rc<T> code example by wrapping Rc<T> around RefCell<T> when defining the Cons:

#[derive(Debug)]
enum List {
  Cons(Rc<RefCell<i32>>, Rc<List>), Nil,
}
use List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
   let data = Rc::new(RefCell::new(10));
 
   let _first_list = Rc::new(Cons(Rc::clone(&data), Rc::new(Nil)));
 
   let _second_list = Cons(Rc::new(RefCell::new(9)), Rc::clone(&_first_list));
 
   let _third_list = Cons(Rc::new(RefCell::new(10)), Rc::clone(&_first_list));
 
   *data.borrow_mut() += 20;
 
   println!("first list after = {:?}", _first_list);
   println!("second list after = {:?}", _second_list);
   println!("third list after = {:?}", _third_list);
}

The code above implements the following:

  • Creates data with Rc<RefCell<i32>> defined in the Cons
  • Creates _first_list with shared ownership as data
  • Creates two other lists, _second_list and _third_list, that have shared ownership with the _first_list
  • Calls the borrow_mut() function (which returns the RefMut<T> smart pointer) on the data and uses the dereference operator * to dereference Rc<T>, get the inner value from the RefCell, and mutate the value

Note that if you do not include the #[derive(Debug)] as the first line in your code, you will have the following error:

Debug Error

Once the code runs, the values of the first list, second list, and third list will be mutated:

Mutated List

Conclusion

You’ve come to the end of this article where you learned about smart pointers, including their use cases. We covered how smart pointers work in Rust and their traits (Deref trait and Drop trait). You also learned about some of the types of smart pointers and use cases in Rust, including Rc<T>, Box<T>, and RefCell<T>.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Deborah Emeni I'm a software developer and technical writer who specializes in Node.js and JavaScript.

Leave a Reply