Treating Smart Pointers like Regular References with the Deref Trait

Implementing Deref trait allows us to customize the behavior of the dereference operator *(as opposed to the multiplication or glob operator). By implementing Deref in such a way that a smart pointer can be treated like a regular reference, we can write code that operates on references and use that code with smart pointers too.

Let’s first take a look at how * works with regular references, then try and define our own type like Box<T> and see why * doesn’t work like a reference. We’ll explore how implementing the Deref trait makes it possible for smart pointers to work in a similar way as references. Finally, we’ll look at the deref coercion feature of Rust and how that lets us work with either references or smart pointers.

Following the Pointer to the Value with *

A regular reference is a type of pointer, and one way to think of a pointer is that it’s an arrow to a value stored somewhere else. In Listing 15-8, let’s create a reference to an i32 value then use the dereference operator to follow the reference to the data:

Filename: src/main.rs

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listing 15-8: Using the dereference operator to follow a reference to an i32 value

The variable x holds an i32 value, 5. We set y equal to a reference to x. We can assert that x is equal to 5. However, if we want to make an assertion about the value in y, we have to use *y to follow the reference to the value that the reference is pointing to (hence de-reference). Once we de-reference y, we have access to the integer value y is pointing to that we can compare with 5.

If we try to write assert_eq!(5, y); instead, we’ll get this compilation error:

error[E0277]: the trait bound `{integer}: std::cmp::PartialEq<&{integer}>` is
not satisfied
 --> <assert_eq macros>:5:19
  |
5 | if ! ( * left_val == * right_val ) {
  |                   ^^ can't compare `{integer}` with `&{integer}`
  |
  = help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for
  `{integer}`

Comparing a reference to a number with a number isn’t allowed because they’re different types. We have to use * to follow the reference to the value it’s pointing to.

Using Box<T> Like a Reference

We can rewrite the code in Listing 15-8 to use a Box<T> instead of a reference, and the de-reference operator will work the same way as shown in Listing 15-9:

Filename: src/main.rs

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listing 15-9: Using the dereference operator on a Box<i32>

The only part of Listing 15-8 that we changed was to set y to be an instance of a box pointing to the value in x rather than a reference pointing to the value of x. In the last assertion, we can use the dereference operator to follow the box’s pointer in the same way that we did when y was a reference. Let’s explore what is special about Box<T> that enables us to do this by defining our own box type.

Defining Our Own Smart Pointer

Let’s build a smart pointer similar to the Box<T> type that the standard library has provided for us, in order to experience that smart pointers don’t behave like references by default. Then we’ll learn about how to add the ability to use the dereference operator.

Box<T> is ultimately defined as a tuple struct with one element, so Listing 15-10 defines a MyBox<T> type in the same way. We’ll also define a new function to match the new function defined on Box<T>:

Filename: src/main.rs

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

Listing 15-10: Defining a MyBox<T> type

We define a struct named MyBox and declare a generic parameter T, since we want our type to be able to hold values of any type. MyBox 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.

Let’s try adding the code from Listing 15-9 to the code in Listing 15-10 and changing main to use the MyBox<T> type we’ve defined instead of Box<T>. The code in Listing 15-11 won’t compile because Rust doesn’t know how to dereference MyBox:

Filename: src/main.rs

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listing 15-11: Attempting to use MyBox<T> in the same way we were able to use references and Box<T>

The compilation error we get is:

error: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

Our MyBox<T> type can’t be dereferenced because we haven’t implemented that ability on our type. To enable dereferencing with the * operator, we can implement the Deref trait.

Implementing the Deref Trait Defines How To Treat a Type Like a Reference

As we discussed in Chapter 10, in order to implement a trait, we need to provide implementations for the trait’s required methods. The Deref trait, provided by the standard library, requires implementing one method named deref that borrows self and returns a reference to the inner data. Listing 15-12 contains an implementation of Deref to add to the definition of MyBox:

Filename: src/main.rs

use std::ops::Deref;

# struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

Listing 15-12: Implementing Deref on MyBox<T>

The type Target = T; syntax defines an associated type for this trait to use. Associated types are a slightly different way of declaring a generic parameter that you don’t need to worry about too much for now; we’ll cover it in more detail in Chapter 19.

We filled in the body of the deref method with &self.0 so that deref returns a reference to the value we want to access with the * operator. The main function from Listing 15-11 that calls * on the MyBox<T> value now compiles and the assertions pass!

Without the Deref trait, the compiler can only dereference & references. The Deref trait’s deref method gives the compiler the ability to take a value of any type that implements Deref and call the deref method in order to get a & reference that it knows how to dereference.

When we typed *y in Listing 15-11, what Rust actually ran behind the scenes was this code:

*(y.deref())

Rust substitutes the * operator with a call to the deref method and then a plain dereference so that we don’t have to think about when we have to call the deref method or not. This feature of Rust lets us write code that functions identically whether we have a regular reference or a type that implements Deref.

The reason the deref method returns a reference to a value, and why the plain dereference outside the parentheses in *(y.deref()) is still necessary, is because of ownership. If the deref method returned the value directly instead of a reference to the value, the value would be moved out of self. We don’t want to take ownership of the inner value inside MyBox<T> in this case and in most cases where we use the dereference operator.

Note that replacing * with a call to the deref method and then a call to * happens once, each time we type a * in our code. The substitution of * does not recurse infinitely. That’s how we end up with data of type i32, which matches the 5 in the assert_eq! in Listing 15-11.

Implicit Deref Coercions with Functions and Methods

Deref coercion is a convenience that Rust performs on arguments to functions and methods. Deref coercion converts a reference to a type that implements Deref into a reference to a type that Deref can convert the original type into. Deref coercion happens automatically when we pass a reference to a value of a particular type as an argument to a function or method that doesn’t match the type of the parameter in the function or method definition, and there’s a sequence of calls to the deref method that will convert the type we provided into the type that the parameter needs.

Deref coercion was added to Rust so that programmers 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 work for either references or smart pointers.

To illustrate deref coercion in action, let’s use the MyBox<T> type we defined in Listing 15-10 as well as the implementation of Deref that we added in Listing 15-12. Listing 15-13 shows the definition of a function that has a string slice parameter:

Filename: src/main.rs

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

Listing 15-13: A hello function that has the parameter name of type &str

We can call the hello function with a string slice as an argument, like hello("Rust"); for example. Deref coercion makes it possible for us to call hello with a reference to a value of type MyBox<String>, as shown in Listing 15-14:

Filename: src/main.rs

# use std::ops::Deref;
#
# struct MyBox<T>(T);
#
# impl<T> MyBox<T> {
#     fn new(x: T) -> MyBox<T> {
#         MyBox(x)
#     }
# }
#
# impl<T> Deref for MyBox<T> {
#     type Target = T;
#
#     fn deref(&self) -> &T {
#         &self.0
#     }
# }
#
# fn hello(name: &str) {
#     println!("Hello, {}!", name);
# }
#
fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

Listing 15-14: Calling hello with a reference to a MyBox<String>, which works because of deref coercion

Here we’re calling the hello function with the argument &m, which is a reference to a MyBox<String> value. Because we implemented the Deref trait on MyBox<T> in Listing 15-12, Rust can turn &MyBox<String> into &String by calling deref. The standard library provides an implementation of Deref on String that returns a string slice, which we can see in the API documentation for Deref. Rust calls deref again to turn the &String into &str, which matches the hello function’s definition.

If Rust didn’t implement deref coercion, in order to call hello with a value of type &MyBox<String>, we’d have to write the code in Listing 15-15 instead of the code in Listing 15-14:

Filename: src/main.rs

# use std::ops::Deref;
#
# struct MyBox<T>(T);
#
# impl<T> MyBox<T> {
#     fn new(x: T) -> MyBox<T> {
#         MyBox(x)
#     }
# }
#
# impl<T> Deref for MyBox<T> {
#     type Target = T;
#
#     fn deref(&self) -> &T {
#         &self.0
#     }
# }
#
# fn hello(name: &str) {
#     println!("Hello, {}!", name);
# }
#
fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

Listing 15-15: The code we’d have to write if Rust didn’t have deref coercion

The (*m) is dereferencing the MyBox<String> into a String. Then the & and [..] are taking a string slice of the String that is equal to the whole string to match the signature of hello. The code without deref coercions is harder to read, write, and understand with all of these symbols involved. Deref coercion makes it so that Rust takes care of these conversions for us automatically.

When the Deref trait is defined for the types involved, Rust will analyze the types and use Deref::deref as many times as it needs in order to get a reference to match the parameter’s type. This is resolved at compile time, so there is no run-time penalty for taking advantage of deref coercion!

How Deref Coercion Interacts with Mutability

Similar to how we use the Deref trait to override * on immutable references, Rust provides a DerefMut trait for overriding * on mutable references.

Rust does deref coercion when it finds types and trait implementations in three 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 first two cases are the same except for mutability. The first case says that if you have a &T, and T implements Deref to some type U, you can get a &U transparently. The second case states that the same deref coercion happens for mutable references.

The last case is trickier: Rust will also coerce a mutable reference to an immutable one. The reverse is not possible though: immutable references will never coerce to mutable ones. Because of the borrowing rules, if you have a mutable reference, that mutable reference must be the only reference to that data (otherwise, the program wouldn’t compile). 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 there was only one immutable reference to that data, and the borrowing rules don’t guarantee that. Therefore, Rust can’t make the assumption that converting an immutable reference to a mutable reference is possible.

results matching ""

    No results matching ""