8.5 KiB
Treating Smart Pointers Like Regular References with the Deref
Trait
Implmenting the Deref
trait allows you to customize the behavior of the dereference operator *
.
This is not the multiplication or glob operator
Implementing Deref
in a way such that a smart pointer can be treated like a regular reference.
You can wirte code that operators on references and use that code with smart pointers as well.
Now lets look at the dereference operator works with regualr references.
Next we will try to define a custom type that behaves like Box<T>
.
Then we will see why the dereference operator doesnt work like a reference on our newly behaves lik Box<T>
.
We will explore how implementing the Deref
trait makes it possible for smart pointers to work in a way that is simialr to references.
Finally we will lokk at Rust's deref coercion feature and how it let us work with either references or smart pointers.
Note: The big difference between the MyBox<T>
type, the custom type we are about to build and the real Box<T>
.
Our version will not store its data on the heap.
We are focusing on the Deref
trait, so where the data is actualy stored is less improtant than the pointer-like behavior.
Following the Pointer to the Value
A regular reference is a type of pointer.
One way to think about a pointer is, an arrow to a value stored somewhere else.
In this example we create a reference to an i32
value.
We then use the dereference operator to follwo the reference to the value.
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
The variable x
holds a 5
(i32
) value.
We then set y
equal to a refernce to x
.
We assert that x
is equal to 5
.
If we want to make an assertion about the value in y
, we have touse *y
to follow the reference to the value it's pointing to.
Hence the need for a dereference.
Once we dereference y
we havce access to the integer value y
is pointing to so that we can compare with 5
If we tired to use assert_eq!(5, t);
instead we woul get the compiler error
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider dereferencing here
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/macros/mod.rs:46:35
|
46| if !(*left_val == **right_val) {
| +
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Comparing a number to a reference is not allowed because they are differnect types.
We must ue the dereference operator to follow the reference to the value they are pointing at.
Using Box<T>
Like Reference
We can rewrite the code from the example to use a Box<T>
instead of a reference.
Here dereference operator using on the Box<T>
functions the same way as the dereference operator used on the reference (from before).
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
The main difference between the two eamples is that we ext y
to be an instance of a Box<T>
pointing to a copied value of x
rather than a reference pointing to the value of x
In the last assertion we can use the dereference operator to following the pointer of the Box<T>
in the way as we deferenced y
when it was a reference.
Defining Our Own Smart Pointer
Here we will bouild a smart pointer simialr to the Box<T>
type provided by the std library.
This is in order to explain fully how smart pointers behave differently from references by defualt.
Then we will look into how to add the ability to use the dereference operator.
The Box<T>
type is defined as a tuple struct with one element.
In this example we defined MyBox<T>
type in the same way.
We will aslo define a new
function to match the new
function defined in Box<T>
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
Here we defined a struct named MyBox
and declarce a generic paramter T
beucause we want our type to hold values of any type.
The MyBox
type is a tuple struct with one element of type T
.
The MyBox::new
function takes one parameter of type T
and returns a MyBox
instance that holds the value passed in.
If we try adding the main
function from above to our preivous examples.
We will change it to use MyBox<T>
type we defined instead of Box<T>
.
The code will not compie because Rust doesn't know how to dereference MyBox
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Here is the resulting compilation error
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
The MyBox<T>
type cant be dereferenced because we havn't implemented the ability to on our type.
To enable dereferencing with the *
operator, we need to implemented the Deref
trait.
Treating a Type Like a Reference by Implementing the Deref
Trait
As discussed before to implement a trait we need to provide implementations for the trait's required methods.
Here the Deref
trait, provided by the std library, requires us to implement one method named deref
that borrows self
and returns a refernce to the inner data.
Here is th implementation of the Deref
to add to the definition of MyBox
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
The type Target = T;
syntax defines an associated type for the Deref
trait to use.
Associated types are a slightly different way of declaring a generic paramter, we will cover them later in ch20.
We fill in the body of the deref
method with &self.0
so deref
returns a reference to the vbalue we want to access with the *
operator.
Recall that using a "Tuple Struct without a Named Filed to Create Different Types" section.
We use .0
accesses the first value in a tuple struct.
The main
function from the example before will now compile and the assertions will pass now with thuis implementation.
Without the Deref
trait, the compiler can only dereference &
references.
The deref
method gives the compiler the ability to take a value of any type that implements Deref
and call the deref
method to get a &
reference that it knows how to deference.
When we write *y
.
Behind the scenes Rust actually ran this code
*(y.deref())
Rust substitues the *
operator with a call to deref
method.
Then a plain derefence so we dont have to think about whether or not we need to call the deref
method.
This Rust feature lets us write code that functions identically regardless of if we have a regular reference or a type that implements Deref
The reason that the deref
method reutnrs a reference to a value, and that the plain dereferncee outside the parentheses in *(y.deref())
is still necessary, is to do with the ownership system.
If the deref
method retuned the value directly instead of a refernece to the value, the value would be moved out of self
.
We dont want to take ownership of the inner value insid MyBox<T>
in this case or in most cases where we use the dereference oerator.
Note: the *
operator is replaced with a call to the deref
method and then a call to trhe *
operator just once.
Each time we use a *
in our code.
Due to the substituation of the *
operator doesn't recurse infinitely we end up with data of type i32
.
This then matches the 5
in assert_eq!
Implicit Deref Coercions iwth Functions and Methods
Deref coercion converts a reference to a type that implements the Deref
trait into a reference to another type.
For example a deref coercion can convert &String
to &str
because String
implemented the Deref
trait such that it returns &str
.