RustBrock/Deref Trait.md
darkicewolf50 856e0e992a
Some checks failed
Test Gitea Actions / first (push) Successful in 17s
Test Gitea Actions / check-code (push) Failing after 14s
Test Gitea Actions / test (push) Has been skipped
Test Gitea Actions / documentation-check (push) Has been skipped
started ch15.2
2025-03-04 11:59:38 -07:00

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.