Deref is a misleading name

Having worked with Rust for a couple of years, I found myself occasionally implementing Deref on some custom types, e.g. if they represent newtype wrappers around other types. I have never questioned the terms like Deref for the trait, its method deref() or Deref coercion. However, after having dug into the implementation and actual actions that the trait's method performs, I believe that deref is a horribly misleading and unfit name for the trait and so is deref coercion.

The trait does not dereference

The first and foremost thing that struck me is, that the trait Deref, respectively its method deref() does not perform dereferencing at all. You can see this clearly by its signature.

We define a type to "dereference" to via type Target;. The method fn deref(&self) -> &Self::Target; then converts a reference to the implementor (&self) to a reference of the target type. That's right Deref::deref() returns a reference, not a dereferenced type.

While the documentation clearly states how Deref works and what its intentions are, this goes against my intuition of a trait being named after the action it performs. Deref does not dereference. It changes how dereferencing a type behaves. Other traits in the standard library are named after what they do. E.g. AsRef returns a reference to target type, Add adds stuff. A better name for it may have been RefMap because it actually maps one reference to another type of reference, similar to AsRef<T>, but with the whole deref coercion magic. Which brings me to the next issue.

Deref coercion is not about dereferencing

If deref coercion was about dereferencing, I'd expect this to work:

use std::ops::Deref;

#[derive(Clone, Copy, Debug)]
struct Foo;

impl Foo {
    fn foo(&self) -> &'static str {
        "I am foo."
    }
}

#[derive(Clone, Copy, Debug)]
struct Bar {
    foo: Foo,
}

impl Deref for Bar {
    type Target = Foo;

    fn deref(&self) -> &Self::Target {
        &self.foo
    }
}

fn print_foo(foo: Foo) {
    println!("{}", foo.foo());
}

fn main() {
    let bar = Bar { foo: Foo };
    print_foo(*&bar);
}

In the line of the call print_foo(*&bar) I'd naively expect the following to happen:

  1. I create a temporary reference to bar, which is of type Bar, that will have type &Bar.
  2. I immediately dereference this reference. Since Bar implements Deref with Target = Foo it should™ dereference to a type Foo.

But no, it doesn't. Because Deref does not actually dereference anything. It just maps reference types. So I'd need to add an extra dereference operator to make the magic happen: print_foo(**&bar); which is equivalent to print_foo(*bar);.
Wait what? bar is not a reference. It is of type Bar, not &Bar. How can I dereference a non-reference type? Well, because due to Deref the code actually desugars to print_foo(*Deref::deref(&bar));. I.e. The Deref trait's method 'deref() gets passed a reference to bar, which it maps to another type of reference, namely &Foo, which then in turn gets dereferenced by the dereference operator *.

On the other hand, the following code works fine:

use std::ops::Deref;

#[derive(Clone, Copy, Debug)]
struct Foo;

impl Foo {
    fn foo(&self) -> &'static str {
        "I am foo."
    }
}

#[derive(Clone, Copy, Debug)]
struct Bar {
    foo: Foo,
}

impl Deref for Bar {
    type Target = Foo;

    fn deref(&self) -> &Self::Target {
        &self.foo
    }
}

fn print_foo(foo: &Foo) {
    println!("{}", foo.foo());
}

fn main() {
    let bar = Bar { foo: Foo };
    print_foo(&bar);
    print_foo(Deref::deref(&bar));
}

There is no dereferencing involved here. We simply take a reference of one type which is automagically ("deref coercion") or explicitly mapped to another reference. Nothing is being dereferenced here.

Where am I going with this?

Probably nowhere. The trait is in standard library for longer than I have been using Rust, so it's not going to go away or be renamed. I rest my case that its name is misleading, as is the term deref coercion. Maybe the official docs can more clearly point out that the trait, despite its name is not about dereferencing and that neither is deref coercion, which rather is reference coercion. Feel free to correct me if I'm wrong with my assertions.

4 Likes

It does dereference, in that the output contains one less layer of indirection. For example, if you have &Arc<T> — two indirections — and call deref() on it, you get &T — one indirection. It just happens that the indirection being removed is not the outermost one.

It would certainly be more natural-looking if it returned T, but that would be the job of the hypothetical DerefMove trait. As it is, we have Deref and DerefMut, which each handle one of the cases of the behavior of the place that the implementing type dereferences to.[1]

As I see it, Deref is correctly named because it is what makes the dereference operator work.
Its effect on deref coercion, method dispatch, etc. is just what follows from making the dereference operator work.


  1. By that line of argument, they might better be named PlaceRef and PlaceMut, but that would probably not be any easier to comprehend and use correctly. ↩︎

7 Likes

it was very interesting to read, but I think your are bit confused, the unary operator * dereference a type but you are explicit overwriting that, and mapping references.

#[derive(Clone, Copy, Debug)]
struct Foo;

impl Foo {
    fn foo(&self) -> &'static str {
        "I am foo."
    }
}

#[derive(Clone, Copy, Debug)]
struct Bar {
    foo: Foo,
}


fn print_foo(foo: Foo) {
    println!("{}", foo.foo());
}

fn main() {
    let bar = Bar { foo: Foo };
    let refBar = &bar;
    print_foo((*refBar).foo);
}

or like this

use std::ops::Deref;

#[derive(Clone, Copy, Debug)]
struct Foo;

impl Foo {
    fn foo(&self) -> &'static str {
        "I am foo."
    }
}

#[derive(Clone, Copy, Debug)]
struct Bar {
    foo: Foo,
}

impl Deref for Bar {
    type Target = Foo;

    fn deref(&self) -> &Self::Target {
        &self.foo
    }
}

fn print_foo(foo: Foo) {
    println!("{}", foo.foo());
}

fn main() {
    let bar = Bar { foo: Foo };
    print_foo(*(bar.deref()));
}

when you

impl Deref for Bar {
    type Target = Foo;

    fn deref(&self) -> &Self::Target {
        &self.foo
    }
}

you are saying instead of dereference Bar, what makes no sense because it is no a &T, you will do this. All you need to do is *_ to a object type Bar to obtain a Foo, not to a reference, if you want to obtain a reference use deref()

Some preliminary observations about the official documentation.

These docs are pretty poor IMO. They're using "deref coercion" as some blanket phrase to cover at least three distinct things:

Their explanation of * is fine, if a bit brief. But that is not deref coercion.

When I explain deref coercion, I emphasize that

  • It takes place "underneath" & or &mut
  • It applies when the target type is known
  • It can insert any number of * applications: &x to &*******x is possible

Method resolution doesn't need a target type, it's a search for a matching method (for some definition of matching). Method resolution applies autoref and does not happen only underneath references, so you may go from x to &x or from &x to x, which deref coercion does not do.

The motivation for Deref implementations that don't reduce indirection is more about method resolution and field access (.) than deref coercion.


The main technical correction related to your intuition I would make is:

Deref is not about changing how * on a &Bar works. It's about defining how * on a Bar works.

The Deref trait is what allows generalizing the * operator from built-in types to any types. Most std implementors are smart pointers of one sort or another, and their implementations do tend to remove a layer of indirection. Using Rc<T> as an example, the implementation defines the * operation on a Rc<T>. It does not change the definition of * on a &Rc<T>.

If the primary motivation is to remove indirection, you may wonder why things work like:

fn deref(&self) -> &Self::Target

*bar ==> *Deref::deref(&bar)
^^^^ the type of that expression is `<_>::Target`

Explanations include

  • We take &self so we don't move owning smart pointers like Box<_> or Rc<_>
  • We can't return Self::Target without moving the target, which we don't want to do
  • We can't return Self::Target when it's not Sized either
  • *bar is a place expression so it can't be the same as just Deref::deref(&bar)

Beyond the technical, I think this is mainly a conversation about being annoyed by a cognitive mismatch.

A lot of your annoyance seems to come from implementations that do not remove indirection, and are more "place conversions" when used with the * operator. But to me that's just a natural consequence of generalizing an operation: whatever the primary motivation, any implementation that matches the Deref signature will do. String + &str concatenates; should we stop calling it Add?

In the generic case, you don't even know how much indirection is or isn't actually being removed. Even in the case of a known foreign type, perhaps tomorrow they'll change their struct to put the target field into a Box to save on inline space. Part of the idea behind traits and generics is you don't have to care.

Assuming you have any interest in changing your perspective, I suggest thinking of Deref as "defining what the * operator does" and as dereferencing in a generic context as "applying the * operator".[2] I think this avoids a lot of the cognitive mismatch.

(There are specific cases where one needs to distinguish Deref based derefencing and built-in dereferencing.)

With the above perspective:

There is no using my_ty + my_ty if you don't impl Add for MyTy, and there is no using *bar if you don't impl Deref for Bar.

And &bar became &*bar, so there was an application of the * operator there.

Deref coercion can certainly be about pointer-chasing dereferencing. But sure, there are also cases that don't remove indirection, but effectively apply an offset... or return some static place even. Again a natural consequence of generalizing an operator / operator overloading.


  1. Also, I would not say T implements Us methods; there is no T::u_method. ↩︎

  2. And similarly for other operator traits. ↩︎

10 Likes

Thanks for all your feedback. My core point was to convey, that the Deref trait actually (insert meme) performs reference mapping and that the deref coercion is more of an internal compiler magic rather than a function of the trait. I understand and agree with most counter points you made, though I still have a different understanding of what the term dereferencing means.