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:
Deref
traitDrop
traitSmart 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:
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
traitThe 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:
20
in a first_data
variablesecond_data
variable uses the reference operator &
to store the memory address of the first_data
variablefirst_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 pointerThe screenshot below shows the output of the code:
Drop
traitThe 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:
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:
small_town
is createdDrop
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)main()
function simply creates two instances of Consensus
and prints the message within the println!
to the screen once they are createdThe screenshot below shows the output of the code:
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>
Rc<T>
smart pointerThe 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:
enum
keyword and List{}
Cons()
to hold a list of reference counted valuesuse
statement for the defined listclone()
function, which creates a new pointer that points to the allocation of the values from the first listRc::strong_count()
functionType 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:
Box<T>
smart pointerIn 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:
stack_data
is stored in a heaphp_data
is stored in the stackIn 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:
RefCell<T>
smart pointerRefCell<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:
data
with Rc<RefCell<i32>>
defined in the Cons
_first_list
with shared ownership as data
_second_list
and _third_list
, that have shared ownership with the _first_list
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 valueNote that if you do not include the #[derive(Debug)]
as the first line in your code, you will have the following error:
Once the code runs, the values of the first list, second list, and third list will be mutated:
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>
.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the 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 application. 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 — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.