Deref trait implementation

use std::ops::Deref;
struct A {
    k: i32,
}

struct B<T> {
    a: T,
}

impl<T> Deref for B<T> {
    type Target = T;

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

impl<T> B<T> {
    fn deref(&self) -> &T {
        &self.a
    }
}
fn main() {
    let b = B { a: A { k: 100 } };
    let k = *b; 
}
error:
   |
25 |     let k = *b;
   |             ^^
   |             |
   |             move occurs because value has type `A`, which does not implement the `Copy` trait
   |             help: consider borrowing here: `&*b`

When clearly in fn deref(&self) function I'm passing the reference of B.a , Why this code gives me error at line let k = *b;

The compiler error message contains the exact reason. A is not Copy, so accessing it by-value tries to move it, and you can't move out of a reference (because what would it point to afterwards?).

my question here is that : when I'm doing something like *b which basically calls deref() and in fn deref(&self) I'm clearly returning a reference. With this logic *b should result into a reference not a value.
To explain my doubt further, if I had implemented struct B like below

 impl<T> B<T> {
    fn deref(&self) -> &T {
        &self.a
    }
 }

then below code would have worked

let k = b.deref();

Even though Deref implementation is something similar to above code then why the same logic wont work for Deref

The catch is that *b is equivalent to *b.deref(), not to b.deref().

1 Like

If that the case then it is quite confusing to be frank

1 Like

Why? Wouldn't you expect dereferencing to remove a level of reference? After all:

let x: i32 = 42;
let ptr: &i32 = &x;
let y: i32 = *ptr; // dereference converts &T into T

The fact that deref() still returns a reference is one of technical necessity. The expression *Deref::deref(value) has to evaluate to a place, otherwise:

  1. Taking a reference to it wouldn't work well, because it would be a reference to a temporary, which is very restricted in its usability; and
  2. DerefMut wouldn't work either, because you couldn't mutate the original value if deref() returned the target by-value (that would only be possible by cloning or copying it, so you would mutate the copy).

I agree it's confusing, I think the trait name is a misnomer because the deref function doesn't actually dereference a reference, instead it changes the type of the pointee.

A better name would have been Coerce.

You should be aware that indexing works in a similar manner; v[0] is approximately *v.index(0).

2 Likes

In the past, I also felt like deref was a misleading name. However, it does indeed "dereference". Even though it doesn't dereference & or &mut types: it does dereference other pointer-like types.

For example: Box<i32> points to an i32. Consequently, &Box<i32> is a reference to a pointer-like type to an integer (double indirection). Deref::deref will convert the &Box<i32> to a &i32, thus removing one level of indirection. It's indeed "dereferencing", though it keeps the outer reference-layer (&) and only works on the referenced value (Box<T>) , dereferencing it (conversion to T), and returning a reference (&T) to the dereferenced value (T).

fn main() {
    // plain value; no references involed:
    let x: i32 = 1;
    // we add one level of referencing:
    let b: Box<i32> = Box::new(x);
    // we add another level of referencing here:
    let b_ref: &Box<i32> = &b;
    // we remove one level of referencing here:
    let i_ref: &i32 = std::ops::Deref::deref(b_ref);
    // we remove one level of referencing here:
    let i: i32 = *i_ref;

    println!("{i}");
}

(Playground)

The confusing part is that when you write x.deref() this may be syntactic sugar for (&x).deref(), which indeed adds one level of referencing (by implicit borrowing) and then removes one level (by deref). But the deref method itself indeed does dereferencing. The borrowing part is part of Rust's syntactic sugar when invoking methods.

use std::ops::Deref;

fn main() {
    let b: Box<i32> = Box::new(1);
    let i_ref: &i32 = b.deref(); // this is actually the same as (&b).deref()
    let i: i32 = *i_ref;
    println!("{i}");
}

(Playground)

3 Likes

It makes sense for smart pointers like Box<T>, Rc<T>, Arc<T>. And actually yes, Deref is also implemented for &T and &mut T and it does dereference in that case.

But what about String and Vec<T>? Are those smart pointers or "pointer-like"? To me it's a stretch to call them that. They do store a pointer, but it seems like an implementation detail rather than the essence of what they represent.

And then what about ArrayVec<T>, ArrayString, ManuallyDrop<T>? Those don't even store a pointer, they are quite clearly not "pointers". And they implement Deref.

That's why I say it is a more general "unwrap" / "coerce" operation that is used implicitly in certain situations, rather than specifically "dereferencing".

Oh right, I forgot about that, but it's pretty important because:

use std::ops::Deref;

fn foo() -> impl Deref<Target = str> {
    //"Hello World!".to_string()
    "Hello World!"
}

fn main() {
    println!("{}", &*foo());
}

(Playground)

If & didn't implement Deref, then we couldn't return a &'static str here.

Well, that depends on the definition of what is "pointer-like". I would say the term is pretty vague. The Rust reference lists the following types as "pointer types":

  • References
    • &
    • &mut
  • Raw pointers
    • *const
    • *mut
  • Smart pointers

Regarding smart pointers, there was this long discussion. I think everyone has their own idea on how that term is used. I personally like a very open definition where a smart pointer actually is something which "refers" in some way to a value (that may be managed by the smart pointer, and where the value could not only be on the heap but, depending on the smart pointer, also be on the stack, which makes deref_owned::Owned and std::borrow::Cow smart pointers as well).

That said, there are other opinions on how the term "smart pointer" should be used. For example you may look at @CAD97's definition here here. If I understand correctly how String and Vec<T> work, then these are not smart pointers according to that stricter definition because it's not possible to go back from a raw pointer to the String or Vec<T>. In contrast, Arc<T> can be converted to a raw-pointer and back to the smart pointer because both the value and the extra data are at a stable heap address and have a known offset difference (see ArcInner). If I'm not wrong, then Vec<T> can't be converted into and back from a raw-pointer because the extra data (like the capacity) can be at an entirely different place in memory.

(But I consider these low-level implementation details that – in my opinion – don't really matter when viewing "pointer-like types" from the perspective of high-level programming.)

Concluding: Whether String and Vec<T> are pointer-like types is a matter of definition in the end.

You are likely not the only one who sees it that way.

However, I feel like a String points to a str (on a very high-level understanding of the term "pointing"). I understand other opinions on that, of course. Note that the Rust book considers String to be a smart pointer in chapter 15:

Though we didn't call them as much at the time, we’ve already encountered a few smart pointers in this book, including String and Vec<T> in Chapter 8. Both these types count as smart pointers because they own some memory and allow you to manipulate it. They also have metadata and extra capabilities or guarantees. String, for example, stores its capacity as metadata and has the extra ability to ensure its data will always be valid UTF-8.

Combining that with the statements in the Rust reference, where smart pointers are "pointer types", one could conclude that String is indeed a pointer type.

I would consider ArrayVec<T> and ArrayString smart pointers, even if they don't allocate on the heap and the memory address of the pointed-to data isn't stable. Not sure about ManuallyDrop<T>. Maybe it's the same as deref_owned::Owned, which I used to consider being a smart pointer in the past.

Again, in the end it's a matter of definition. In the Rust world, things are different than in C.

I think that's a valid p.o.v. Given that the method (and trait) has been called deref (and Deref, respectively), however, it might be helpful to "embrace" that broader definition of dereferencing, pointer-like types, and smart pointers. But yeah, I agree it can be confusing to use the broader definition(s).

1 Like

Wrong link :slight_smile:

My definition IIRC is roughly that a "smart pointer" is any dereferencable type which can be converted to/from its single dereferenced "raw pointer" and thus temporarily "forget to be smart," i.e.

unsafe trait SmartPointer: Deref {
    use <Self as Deref>::Target as Pointee;
           fn into_raw(this: Self) -> *mut Self::Pointee;
    unsafe fn from_raw(ptr: *mut Self::Pointee) -> Self;
}

then with a bunch of extra restrictions on validity/provenance of the pointer.

This is perhaps a bit reductive, and perhaps a deficiency in my initial definition of "smart pointer". Even if they are not themselves "smart pointer"s, they very obviously contain "smart pointer"s; e.g. Vec<T> is logically { ptr: Box<[MaybeUninit<T>]>, init: usize } (although this is much more often inlined and written as { ptr: *mut T, len: usize, cap: usize }).

I think it's fair to say that a "smart pointer" may contain some state before the indirection. Thus the trait would be refined to instead e.g.

unsafe trait SmartPointer: Deref {
    use <Self as Deref>::Target as Pointee;
    type ExtraMetadata;
           fn into_raw_parts(this: Self) -> (*mut Self::Pointee, Self::ExtraMetadata);
    unsafe fn from_raw_parts(ptr: *mut Self::Pointee, extra_meta: Self::ExtraMetadata) -> Self;
}
2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.