2024-11-07 (last edit: 2022-11-21)
So far we've only used heap allocated memory indirectly by working with containers such as vectors, maps or the String
type, otherwise allocating our variables on the stack. We didn't really have to be aware of the fact that these collections used the heap, as all that memory management details were hidden away from us. In this lesson we'll take a closer look at what is really happening there and how we can do that ourselves.
To work with heap-allocated memory, Rust features smart pointers. You should have already heard this term as it is a very important feature in C++ and the concept is virtually the same here - they are wrappers around raw allocated memory that provide additional, safety-ensuring mechanism. What defines a smart pointer in Rust is generally the implementation of two traits: Drop
and Deref
.
The Drop
trait is pretty straightforward as it consists of one method - fn drop(&mut self)
- that is, basically, the destructor, invoked during stack unwinding.
The Deref
trait allows us to overload the dereference (*
) operator.
Apart from enabling access to the underlying value, implementing the Deref
trait enables Rust to perform deref coercion on the pointer - trying to remove as many levels of indirection as it can. What it means in practice is that we will be able to use it with any code working on plain references.
use std::ops::Deref;
struct MyBox<T>(T);
// We won't be allocating anything on the heap here as it is not important here.
// We're only focusing on the dereference mechanisms.
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let x = 5;
let int_box = MyBox::new(x);
assert_eq!(5, *int_box);
// String also implements the `Deref` trait.
// In fact, String actually is a smart pointer.
let s = String::from("I'm a smart pointer too");
hello(&s);
// Deref coercion can deal with multiple levels of indirection.
let str_box = MyBox::new(String::from("Rust"));
hello(&str_box);
}
(Download the source code for this example: deref_coercion.rs)
In general, there are three possible coercions that Rust can perform:
From &T
to &U
when T: Deref<Target=U>
From &mut T
to &mut U
when T: DerefMut<Target=U>
From &mut T
to &U
when T: Deref<Target=U>
While the first two coercions are straightforward, the third one is possible because treating a mutable reference as an immutable one does not break the rules of ownership.
Box
- simple wrapperThe Box<T>
type is the most basic out of Rust's smart pointers, equivalent to C++'s std::unique_ptr<T>
. It's a simple wrapper that makes sure the underlying memory gets allocated and freed properly.
fn box_simple() {
let b = Box::new(5);
println!("b = {}", b);
let _x = 10 + *b;
}
// `Box` gives us the indirection required to define
// recursive types
#[allow(dead_code)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
box_simple();
}
(Download the source code for this example: box.rs)
The Rc<T>
type is the equivalent of std::shared_ptr<T>
from C++. There is one caveat to this though - because we're creating multiple references to the same object, those references have to be immutable in accordance with the ownership rules.
use std::rc::Rc;
struct LoudInt(i32);
impl Drop for LoudInt {
fn drop(&mut self) {
println!("[{}] Farewell!", self.0);
}
}
fn main() {
{
let outer_ref;
{
let inner_ref = Rc::new(LoudInt(5));
// strong_count represents the number of owning references pointing
// to data
assert_eq!(Rc::strong_count(&inner_ref), 1);
outer_ref = Rc::clone(&inner_ref);
assert_eq!(Rc::strong_count(&inner_ref), Rc::strong_count(&outer_ref));
assert_eq!(Rc::strong_count(&inner_ref), 2);
}
println!("The {} still lives!", outer_ref.0);
assert_eq!(Rc::strong_count(&outer_ref), 1);
}
}
(Download the source code for this example: ref_count.rs)
Rust also provides a non-owning pointer in the form of Weak<T>
(equivalent to std::weak_ptr<T>
) that can be obtained from an instance of Rc<T>
.
use std::rc::Rc;
struct LoudInt(i32);
impl Drop for LoudInt {
fn drop(&mut self) {
println!("[{}] Farewell!", self.0);
}
}
fn main() {
let weak_ref;
{
let shared_ref = Rc::new(LoudInt(5));
// weak_count keeps track of the non-owning reference to the data
assert_eq!(Rc::weak_count(&shared_ref), 0);
// `downgrade()` obtains a weak pointer to Rc's data
weak_ref = Rc::downgrade(&shared_ref);
assert_eq!(Rc::weak_count(&shared_ref), 1);
assert_eq!(Rc::strong_count(&shared_ref), 1);
// In order to use the the data underneath the weak pointer
// we need to obtain a new shared pointer from it.
// The `upgrade()` method returns `Option<Rc<T>>`.
let temp = weak_ref.upgrade();
assert_eq!(Rc::strong_count(&shared_ref), 2);
println!("The value is {}", temp.unwrap().0);
}
println!("The value should be deallocated by now.");
assert!(weak_ref.upgrade().is_none());
}
(Download the source code for this example: weak_ref.rs)
Good examples and explanation of the interior mutability pattern and runtime borrow checking can be found in the book.
Alongside the RefCell<T>
type described above, there is an analogous Cell<T>
type that operates on values instead of references.
dyn
objectsIn previous labs you learned about dynamic dispatch and its strengths. The largest drawback you noticed is most likely that they are unsized (!Sized
, where !
being syntax signifying lack of trait implementation).
When storing an object on a heap, however, we can use it as a dyn
object seamlessly.
std::borrow::Cow, a versatile copy-on-write smart pointer
Deadline: 13.11.2024 23:59