Rust
: Comparative analysis withC++
Published on March 17, 2025
This article aims to provide an analysis of Rust ownership and related concepts for programmers with experience in C++.
Borrowing a value means having a reference to the value without owning it.
Taking ownership of a value means becoming the new owner of the value.
let m_str = "Hello, World".to_string(); // m_str owns the value
let m_str_ref = &m_str; // m_str_ref borrows the value
let m_str2 = m_str; // m_str2 becomes the new owner, m_str is invalidated.
println!("{}", m_str_ref); // Ok: It is a reference
println!("{}", m_str2); // Ok: It is the new owner
println!("{}", m_str); // Error: Ownership has moved to m_str2.
// The binding of m_str has been dropped (moved to m_str2)
clone()
method.Rust
std::rc::Rc<T>
Rc<T>
is a smart pointer that allows multiple ownerships. If you’re familiar with C++, std::rc::Rc<T>
is similar to std::shared_ptr<T>
.
Both:
- Use a reference-counting mechanism to manage heap-allocated data.
- Automatically deallocate the object when the last reference is dropped.
- Cloning or copying an Rc<T>
creates a new reference to the same object and increases the reference count. Similarly, copying a shared_ptr<T>
does the same.
Let's look at an example in Rust
to demonstrate multiple ownership.
See this example on Compiler Explorer here.
use std::rc::Rc; // Import reference counter
fn main() {
let owner = Rc::new(8); // Create a new reference counter
println!("Owners: {}", Rc::strong_count(&owner));
{ // Closure
println!("New closure (1)");
let _owner2 = owner.clone();
println!("Owners: {}", Rc::strong_count(&owner));
{ // Another closure
println!("New closure (2)");
let _owner3 = owner.clone();
println!("Owners: {}", Rc::strong_count(&owner));
println!("Leave closure (1)");
}
println!("Owners: {}", Rc::strong_count(&owner));
println!("Leave closure (2)");
}
println!("Owners: {}", Rc::strong_count(&owner));
}
Running this program gives the following result:
Owners: 1
New closure (1)
Owners: 2
New closure (2)
Owners: 3
Leave closure (1)
Owners: 2
Leave closure (2)
Owners: 1
This illustrates two things: - The reference counter increments when adding a new owner. - The reference counter decrements when the program leaves the scope of an owner.
std::rc::Rc<T>
and std::shared_ptr<T>
Feature | Rc<T> (Rust) |
std::shared_ptr<T> (C++) |
---|---|---|
Thread Safety | Not thread-safe (modifying the reference count at the same time can cause data races and undefined behavior). Arc<T> is used for multi-threading. |
Thread-safe for reference counting (atomic operations). Not thread-safe for managed T object (needs to use mutex). |
Performance | Faster (no atomic operations) | Slightly slower (due to atomic reference counting) |
Box<T>
Box<T>
provides ownership of heap-allocated data and ensures that the data is deallocated when the Box
goes out of scope.
Box<T>
is comparable to C++ std::unique_ptr<T>
:
- Both enforce unique ownership.
- C++ is less strict, as it doesn't detect moved ownership at compile time:
Here’s an example showing a runtime error in C++ when trying to access a moved value:
#include <memory>
#include <iostream>
using namespace std;
int main() {
auto ptr1 = make_unique<int>(10);
cout << "Ptr1 value: " << *ptr1 << endl;
auto ptr2 = move(ptr1);
cout << "Ptr2 value: " << *ptr2 << endl;
// Trying to access ptr1 after the move
// Compiles, but causes a SIGSEGV error (runtime error)
cout << "Ptr1 (2) value: " << *ptr1 << endl;
}
In comparison, the following Rust code won’t compile at all:
fn main() {
// Create a `Box` to hold an integer on the heap
let ptr1 = Box::new(10);
println!("Ptr1: {}", *ptr1);
let ptr2 = ptr1;
println!("Ptr2: {}", *ptr2);
println!("Ptr1 (2): {}", *ptr1); // Compile error
}
This results in the following error message:
error[E0382]: borrow of moved value: `ptr1`
--> <source>:16:29
|
12 | let ptr1 = Box::new(10);
| ---- move occurs because `ptr1` has type `Box<i32>`, which does not implement the `Copy` trait
13 | println!("Ptr1: {}", *ptr1);
14 | let ptr2 = ptr1;
| ---- value moved here
15 | println!("Ptr2: {}", *ptr2);
16 | println!("Ptr1 (2): {}", *ptr1);
| ^^^^^ value borrowed here after move
Arc<T>
Arc<T>
is used to enable shared ownership of data between multiple threads. The reference count is atomically updated, making it safe for concurrent access.
- Thread-safe (atomic reference counting), unlike Rc<T>
.
- It is immutable by default.
Here’s an example using Arc
in a multi-threaded context:
use std::sync::Arc;
use std::thread;
fn main() {
let value_ptr = Arc::new(5);
let value_ptr_clone = Arc::clone(&value_ptr);
let handle = thread::spawn(move || {
println!("Inside thread, value: {}", value_ptr_clone);
});
handle.join().unwrap();
println!("Main thread, value: {}", value_ptr);
}
Arc<T>
doesn’t allow direct mutation of the value because it is immutable by default. This is a major difference compared to C++, where mutable data can be shared across threads. Rust’s emphasis on immutability helps eliminate the risk of modifying the same value from different threads.
Mutex<T>
for MutabilityTo modify a value within a thread, we need to combine Arc<T>
with a Mutex<T>
(or RwLock<T>
, though we won’t cover RwLock
here). With Mutex<T>
, we must lock the value inside the thread before making any modifications. Below is an example using Arc<Mutex<T>>
:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let value_mutex_ptr = Arc::new(Mutex::new(5)); // Using Mutex inside Arc
let value_mutex_ptr_clone = Arc::clone(&value_mutex_ptr);
let handle = thread::spawn(move || {
let mut data_mutex = value_mutex_ptr_clone.lock().unwrap(); // Lock the mutex
*data_mutex = 42; // Modify the value
println!("Inside thread: {}", *data_mutex);
});
handle.join().unwrap();
let data_mutex = value_mutex_ptr.lock().unwrap();
println!("Inside main thread: {}", *data_mutex);
}
Rust
to take ownership of a value and borrow a value.std::rc::Rc<T>
, a smart pointer that allows multiple ownership through reference counting. It is efficient but not designed for multi-threading.std::sync::Arc<T>
to share ownership across threads. Arc<T>
is immutable by default.Arc<T>
, we combine it with Mutex<T>
(e.g., Arc<Mutex<T>>
).Rust C++
No comments yet. Be the first to comment!