How * in Rust can get the actual value of &T?

Hi guys, I am new to Rust. I have some questions about the concept of "Dereferencing" using *

I have read some posts and answers on this site, but things still seem unclear to a newbie like me. My questions are as follows:

How * in Rust get the actual value of a reference? While i see the definition of * (deref method from std::ops::Deref) will return a reference to a type T (&T), not the actual value. But when i use *, the result i get is the actual value, not the reference to that actual value.

If my question is not clear, or I have missed anything in this post, I hope everyone will give me feedback so I can change it. Thanks everyone.

1 Like

*v is equivalent to *Deref::deref(&v). That is, the deref() method isn't the dereference operator itself but part of how it is implemented. The actual dereference of the reference returned is built in to the compiler.

1 Like

I agree with you on the statement "*v is equivalent to *Deref::deref(&v). That is, the deref() method isn't the dereference operator itself but part of how it is implemented".

The result of the line "Deref::deref(&v)" -> "(&T)". This is where my question comes in. As I understand it, * is a method defined inside std::ops::Deref. And the result of this method is a "Reference to a Value (&T)", not an Actual Value. This is the real question I have, but I have found few satisfactory explanations for this problem (or maybe I don't have enough knowledge to understand what people have explained) :laughing:

#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_unstable(feature = "const_deref", issue = "88955")]
impl<T: ?Sized> const Deref for &T {
    type Target = T;

    #[rustc_diagnostic_item = "noop_method_deref"]
    fn deref(&self) -> &T {
        self
    }
}

This is what * looks like when I use "ctrl + click" on *

So I think * is actually this method, and when we use * Rust is actually calling this method.

As I said, the dereference of the reference types & and &mut is built in to the compiler. The Deref trait exists to allow using the dereference operator with other smart pointers, and it uses the dereferencing of references as part of how it does that. A smart pointer just needs to explain how the thing it points to can be expressed as a reference and the compiler can then get the value out.

3 Likes

It is not the case that * is always implemented as *Deref::deref(&v). If it was, then all uses of * would be infinite recursion. The Rust Reference states it more precisely:

When applied to a pointer it denotes the pointed-to location.

On non-pointer types *x is equivalent to *std::ops::Deref::deref(&x) in an immutable place expression context and *std::ops::DerefMut::deref_mut(&mut x) in a mutable place expression context.

References are pointers, so they get the built-in primitive dereferencing that refers to memory directly. The job of Deref and DerefMut is to convert all other (non-pointer-type) dereferencing into pointer dereferencing. The reason that there is a Deref implementation for &T is so that generic code that wants <P: Deref> can accept plain references P = &T as well as library-defined types like P = Rc<T>.

Imagine a world where we had two differently named dereference operators: *<primitive> (which is always the compiler built-in pointer access) and *<call_deref> (which always calls Deref). In that world, we could say that *<call_deref>(v) is implemented as *<primitive>(Deref::deref(&v)).

12 Likes

Could you please elaborate on your statement

dereference of reference types & and &mut is built-in to the compiler

Do you mean that for non-pointer types, when we use *x, Rust will execute the following command "*std::ops::Deref::deref(&x)".

And for pointer types (Box, String,...), Rust will only call the deref() method that has been implemented for that type, right? So *x for pointer types will be equivalent to "std::ops::Deref::deref(&x)" or not? Does Rust need to add * before "std::ops::Deref::deref(&x)"?

Please don't laugh at my question :melting_face:

For &_, &mut _ and -- at least to date -- Box<_>, no. For those it is a primitive operation.[1]

Box<_> has special language-level support, but String does not. So dereferencing for a String is *Deref::deref(&x).[2]


  1. also for *const _ and *mut _ (only allowed in unsafe) ↩︎

  2. or *DerefMut::deref_mut(&mut x) ↩︎

2 Likes

Could you please elaborate on your statement.
I still can't quite understand

Box<_> has special language-level support, but String doesn't. So the dereferencing for a String is *Deref::deref(&x)

I think the overall idea is:

  • We have Pointer types &
  • And Smart Pointers like Box, String, Rc etc.

For Pointer types like any &, the operator * is the lowest level we describe. No conversion involving Deref::deref() happens.

This applies to Box as well.

Think of it like a + operation for numbers.

For smart pointers (excepting Box) * converted into Deref::deref() in the way you described.

The key point is that once Deref::deref() returns a reference (&) then the process ends, i.e with *. (Because we got rid of smart pointers in previous dereferences.)

PS: you should take a look at chapter 15 creating your own Box if you havent yet.

2 Likes

Conceptually it's like at some point during compilation, the compiler sees...

SomeContext {
   *some_value
}

...and does some translation process internally that's like...

// (pseudo-code)
let expression = match (SomeContext, type_of(some_value)) {
    // Built in operation for special types
    //   (When I talk about "special types" below I'm talking about these
    //    particular types with built-in primitive * operations)
    (_, &_)
    | (_, &mut _)
    | (_, *const _)
    | (_, *mut _)
    | (_, Box<_>) 
    => *<primitive> some_value,

    // Use the traits for all other types
    (Shared, _) => *<primitive> <_>::deref(&some_value),
    (Exclusive, _) => *<primitive> <_>::deref_mut(&mut some_value),
}

...and depending on the context and the type of some_value, you end up with one of:

SomeContext {
   *<primitive> some_value
}
SomeContext {
   *<primitive> <_>::deref(&some_value)
}
SomeContext {
   *<primitive> <_>::deref_mut(&mut some_value)
}

After this transformation, all remaining dereference operations are primitive (built-in) dereference operations.


Box<_> is a special type that has a primitive dereference operation to allow you to move out of a Box<_> with a dereference and so on -- "magic abilities" that we (so far) do not have traits for.

String is not a special type, so dereferences of String go through the Deref traits.

4 Likes

Thank you very much, but I may not have enough knowledge to understand what you mentioned above (please don't think that I don't like your answers. I really appreciate your answers. But I know that I may not have enough knowledge) :smiling_face_with_tear:

Thank you very much again.

Thanks for this reply, I will ponder what you said :wink:

Thank you very much.

An analog:

When you click + in 1 + 2, you see + is implemented as std::ops::Add, which is implemented as + for i32. You (probably) won’t question how Rust compute 1 + 2, the compiler just know. But for more complex type like String::from("1") + "2", the compiler itself does not know how to do it, so it invokes Add::add for that.

It’s pretty much the same for *. The compiler just know how it works for some primitive types (&, &mut, Box). And for complex types, *T invokes Deref::deref.

1 Like

Nitpick though - this describes a one level of smart-pointers, but we can have more. E.g. this code calls Deref::deref twice:

fn deref<'a>(x: &'a MutexGuard<'_, String>) -> &'a str {
    &*x
}

...first to get from MutexGuard<String> to String, then from String to str.

2 Likes

Others have given some good explanations of this already, but what I mean is that if you have r of type &T or &mut T for some type T, then *r isn't defined in terms of Deref (although & and &mut still implement it for use in generics). The Rust compiler just knows that &T and &mut T represent memory addresses pointing to a value of type T[1], so it can just translate it to an access to whatever is stored at that memory address.


  1. plus some metadata if T is !Sized ↩︎

1 Like

I believe here the * calls Deref::deref only once, and the second call is due to a separate deref coercion when returning.

2 Likes

No worries. You asked a good question and the answer isnt that easy :slight_smile:

1 Like

Thanks for your time and patience in helping me.

I am still lacking in knowledge, so please forgive me for some bad questions :smiling_face_with_tear:

Thanks again for the answers guys.