skip to content
Erick

Why is using smart pointers important in Rust?

/ 3 min read

Example: Using Rc in a Single-Threaded Context

Consider the following example with Rc, a smart pointer designed for single-threaded use cases:

use std::rc::Rc;
struct Person {
age: u8,
name: String,
birth: u32,
}
fn show_name(person: Rc<Person>) {
println!("{}", person.name);
}
fn main() {
let person = Rc::new(Person {
age: 22,
name: String::from("Erick"),
birth: 2002
});
show_name(person.clone()); // Increment reference counter
assert_eq!(person.age, 22); // Use the original object safely
}

In this code, the show_name function accepts an Rc<Person>. Instead of transferring ownership of the data or creating a deep copy, we clone the Rc, incrementing its reference counter. The data remains in memory as long as there’s at least one reference to it.

Memory Layout

Let’s look at the memory addresses when working with Rc:

use std::rc::Rc;
fn show_name(person: Rc<Person>) {
println!("{}", person.name);
println!("show_name: {:p}", &person);
}
fn main() {
let person = Rc::new(Person {
age: 22,
name: String::from("Erick"),
birth: 2002
});
show_name(person.clone()); // Increments reference counter
println!("main: {:p}", &person);
}

Output:

Terminal window
Erick
show_name: 0x7ffdef0edbf8
main: 0x7ffdef0edd48

Here, show_name takes ownership of the Rc instance but only increments the reference count instead of duplicating the data. This behavior is called a shallow copy, where only the reference is copied, leaving the actual data untouched.


Why Use Arc for Multithreaded Programs?

While Rc works well in single-threaded contexts, it is not thread-safe. For shared data across threads, we use Arc.

Example: Arc in Multithreading

Here’s how Arc ensures thread-safe sharing of data:

use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let threads = (0..3)
.map(|i| {
let data = data.clone(); // Each thread increments the counter
println!("Reference count: {}", Arc::strong_count(&data));
thread::spawn(move || {
println!("Thread {}: {:?}", i, data);
})
})
.collect::<Vec<_>>();
for t in threads {
t.join().unwrap();
}
}

Output:

Terminal window
Reference count: 2
Reference count: 3
Reference count: 4
Thread 0: [1, 2, 3]
Thread 1: [1, 2, 3]
Thread 2: [1, 2, 3]

Each thread clones the Arc, incrementing its reference count. When the threads finish execution, the reference count drops to zero, and the data is deallocated.


Key Benefits of Using Smart Pointers

  1. Memory Efficiency: Avoids deep copies by reusing data and incrementing reference counts.
  2. Thread Safety: With Arc, shared data can be safely accessed across multiple threads.
  3. Automatic Deallocation: When the reference count drops to zero, the data is automatically freed, preventing memory leaks.

Deep Dive: Reference Counting

use std::rc::Rc;
fn main() {
let person = Rc::new(Person {
age: 22,
name: String::from("Erick"),
birth: 2002,
});
let person2 = person.clone(); // Increments counter
let person3 = person.clone(); // Increments again
println!("Reference count: {}", Rc::strong_count(&person)); // 3
}

In this example, the reference count reflects the total number of Rc instances pointing to the same data. Once all references go out of scope, the memory is freed automatically.


Conclusion

Using Rc and Arc for managing shared data in Rust is not only memory-efficient but also aligns with Rust’s core guarantees of safety and performance. By leveraging smart pointers, you can minimize memory duplication while ensuring safe, concurrent access where necessary. This practice is widely encouraged in Rust and should be part of every developer’s toolbox.