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?
- How smart pointers work in Rust
Deref
traitDrop
trait- Types of smart pointers in Rust and their use cases
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 afirst_data
variable - The
second_data
variable uses the reference operator&
to store the memory address of thefirst_data
variable - A condition that checks if the value of the
first_data
is equal to the value of thesecond_data
. The dereferencing operator*
is used on thesecond_data
to get the value stored in the memory address of the pointer
The screenshot below shows the output of the code:
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:
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 thedrop()
method with the mutable reference is implemented with theimpl
keyword on the struct. The message within theprintln!
statement is printed to the console when the instances within themain()
function go out of scope (that is, when the code within themain()
function finishes running) - The
main()
function simply creates two instances ofConsensus
and prints the message within theprintln!
to the screen once they are created
The screenshot below shows the output of the code:
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 andList{}
- 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:
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:
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
withRc<RefCell<i32>>
defined in theCons
- Creates
_first_list
with shared ownership asdata
- Creates two other lists,
_second_list
and_third_list
, that have shared ownership with the_first_list
- Calls the
borrow_mut()
function (which returns theRefMut<T>
smart pointer) on the data and uses the dereference operator*
to dereferenceRc<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:
Once the code runs, the values of the first list, second list, and third list will be mutated:
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>
.
The post Understanding smart pointers in Rust appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/PDe7dfg
Gain $200 in a week
via Read more