Referencing and equality

I'm buried deep in (3) different books trying to learn Rust, and this still confuses me.

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

assert_eq!(5, x);
assert_eq!(5, *y);
   
println!("{}, {}, {:p}", x, y, &x); 

}

Result:

5, 5, 0x7ffdc60edca4

////////////////////////////////
This messes with my transitive-property-laden brain.

if "y" is truly equal to "&x", then why wouldn't println! evaluate "y" to "0x7ffdc60edca4"?
Why is it that if one wants "y" to equal "5", the assignment wouldn't be something like "let y = *&x" (that is,"y" equals the data stored at the address of x) analogous to the need to dereference "y" in "assert_eq!" ?

Because you told it to :p that last one, but not the others. So because you asked for it to be formatted in a different way, it did.

markdown please Markdown Cheatsheet · adam-p/markdown-here Wiki · GitHub

Yes, thanks for responding, however, but that doesn't address the nature of my question.

In Rust, a == b implies &a == &b (and thus &&a == &&b, et cetera).[1] That is, == isn't going to give you pointer equality. You can compare raw pointers instead if needed. (Though pointer equality has it's own downfalls, especially when zero-sized types (ZSTs) are involved.)

Similarly, the println! macro / the formatting system in stdlib generally will see through all layers of references, unless you tell it not to ({:p}).

In your example you're only asserting value equality and only asking to print an address with the last argument.


  1. The depth of the references has to be equal due to some limits of design in how it is implemented, but if possible, it would also imply a == &b and so on. ↩︎

3 Likes

Yes, that makes a bit more sense. the println! macro, as you say ". . . will see through all layers of references. . .". The confusing part is why assert_eq! does not (without the help of *). It seems as though there is a lack of congruency here. ?
I'm trying to get a better feel for how these macros/expressions, etc. are actually interacting with the memory.

That's referenced in the note - essentially, you can't compare T with &T, due to the current limitations; therefore, you can't pass T and &T to assert_eq!, since it literally uses == to do the check.

1 Like

I wrote up a whole thing which, after seeing @Cerber-Ursi's reply, I think is about me misunderstanding what you were getting at, but anyway:

A whole thing

Hmm, well

  • x is an i32 with value 5
  • y is an &i32 pointing at x
  • *y is a *&i32 is an i32 with whatever value is pointed at (5)

So assert_eq!(5, *y) is assert_eq!(5, 5) which succeeds.

assert_eq!(&5, y) would defer to assert_eq!(*&5, *y) which would be assert_eq!(5, 5) again.


Even more accurately, in the playground under Tools, you can get a view of the expanded macros (or on the command line with something like cargo expand. [1] So we can see that assert_eq!(5, *y) expands into something like

    match (&5, &*y) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                    let kind = ::core::panicking::AssertKind::Eq;
                    ::core::panicking::assert_failed(kind, &*left_val,
                        &*right_val, ::core::option::Option::None);
                }
        }
    };

It's just going to use whatever == would use, which in this case is "dereference until you hit a non-reference and then compare those values".

The whole formatting mechanism is less decipherable and direct, but again a &T or &&&&&&&&&T is going to format the same as a T unless you use the {:p} flag.


Neither of the "see through reference" capabilities are magic; that's how they define the fmt traits and the PartialEq trait.

In particular, we can see this blanket implementation and if you click source you'll see it just compares the derefences, and for fmt from here or similar you can see

fmt_refs! { Debug, Display, Octal, Binary, LowerHex, UpperHex, LowerExp, UpperExp }

and then control-f for fmt_refs to find this where again it is just deferring to the dereferenced version.

In contrast with the Pointer implementation which calls the *const T version for &T which ultimately formats ptr.addr().

As for the *const T implementation for PartialEq, only here do we hit "magic" -- a compiler built-in is really what performs pointer equality (or this implementation would self-recurse). I tried to find the PR for you but it's ancient in Rust years.

See also core::ptr::eq.


But really, if you're messing around with addresses and pointer values, there's a lot of footguns for one and you're probably in unsafe Rust using *const T or *mut T for another. In typical safe Rust, if you have a &T or a &&&&T, you probably only really "care" about the value of the T.


Looking at the implementation of PartialEq for references, you can see that it is for comparing &B to &A when you can compare B to A, i.e. the same number of references have to be added to both sides.


  1. Though the expansion doesn't capture all the hygienic considerations of macros apparently, it will give you a good idea. ↩︎

2 Likes

That's because assert_eq!() will use == for comparing values. For example, if I am comparing two variables, a and b, the compiler will look for a impl PartialEq<typeof(b)> for typeof(a)[1] implementation that it can use, and only that implementation. There is no blanket implementation when one side is a reference and the other is a type, so this will emit a compile error unless you dereference the reference (the * bit).

On the other hand, the code println!() expands to allows deref coersion to "see through" the references and find a std::fmt::Debug implementation that can be used to print your thing. That means if it can't find a impl Debug for &Foo it'll also look for a impl Debug for Foo and automatically insert the dereference.


  1. typeof doesn't actually exist, but hopefully you get what I mean. ↩︎

1 Like

Minor correction: println! & friends don't rely on deref coercion to display &Foo as Foo. The formatting machinery just finds the implementation of Debug for &Foo, which this blanket impl delegates to Foo.

All the formatting traits are implemented for references in this way, except Pointer, since its whole job is to be concerned with the pointery aspect of things.

6 Likes