RustBrock/Deref Trait.md
2025-03-04 14:13:02 -07:00

13 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.

Deref coercion is a convenience that Rust perfomrs on args to functions and methods.

This only works on tpyes that implement the Deref trait.

It happens automatically when we pass a reference to a particular type's value as an arg to a function or method that doesn't match the parameter type in the function or method definition.

A sequence of calls to the deref method converts the type we provided into the type the parameter needs.

Deref coercion was added to Rust so that rogrammers writing function and method calls don't need to add as many explicit references and dereferences with & and *.

This feature also lets us write more code that can code that can work for either references or smart pointers.

To see deref coercion in action we will use MyBox<T> type defined previously with the implementation of Deref as well.

This example shows the definition of a function that has a string slice parameter

fn hello(name: &str) {
    println!("Hello, {name}!");
}

We can call the hello function with a string slice as an arg.

Such as hello("Rust"); as an example.

With Deref coercion makes it possible to call hello with a reference to a value of type MyBox<String>

Here is an example of this

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

Here we call the hello function with the arg &m, which is a reference to a MyBox<String> value.

Due to us implementing the Deref trait on MyBox<T>, Rust can turn &MyBox<String> into a &String by calling deref.

The std library then proves an implementation of Deref on String that reutnrs a string slice.

This is in the API docs for Deref.

Rust then calls deref again to turn the &String into &str. This matches the hello function definition.

If Rust didn't implement deref coercion we would have to wrtie does like this instead to call hello with a value of type &MyBox<String>

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

The (*m) dereferences the MyBox<String> into a String.

The & and [..] takes a string slice of the String that is equal to the whole string to match the signature of hello.

This code without deref coercions is much harder to read, wrtie and understand, escpially with all of the symbols involved.

Deref coercion allows Rusts to handle all of these conversions automatically.

When the Deref trait is defined for the types involved.

Rust will analyze the types and use Deref::deref as many times as necessary to get a reference to match the param type.

The number of times that Deref::deref needs to be inserted is resolved at compile time.

There is no runtime penalty for taking advantage of deref coercion.

How Deref Coercion Interacts with Mutability

Similar to how you would use Deref to override the * operator on immutable references, you can use the DerefMut trait to override the * operator on mutable references.

Rust does deref coercion when it finds tpyes and trait implelemtations in there cases

  • 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>

The fist two cases are the same expect that the second implements mutablility.

The first one states that if you have a &T and T implements Deref to some type U you can get a &U transparently

The second states that the smae deref coercion happens for mutable references.

The third one is tricker.

Rust will also coerce a mutable reference to an immutable one.

The reverse is not possible: immutable references will never coerce to mutable refernces.

This is due to the borrowing rules, if you have a mutable reference, that mutable reference must be the only reference to that data.

(The program will not compile if otherwise)

Converting one mutable reference to one immutable reference will never break the borrowing rules.

Converting an immutable reference to a mutable reference would require that the intial immutable reference is the only immutable reference to that data.

Borrowing rukes don't gaurantee that.

Therefore Rust can't make that assumption that converting an immutable reference to a mutable reference is possible.