Why can we not compare Values & References?

Say we have this code:

    let val:      u32 = 12345;
    let val_ref: &u32 = &val;

    let is_equal = val == *val_ref;
    println!("{}", is_equal);

I am trying to understand why Rust cannot compare val == val_ref, and why we need to write val == *val_ref instead? E.g. in C++ we can compare const int (value) and const int& (reference). Of course, we cannot compare const int (value) and const int* (ptr). So in Rust, is &u32 actually a pointer type? Are there no "references" as such?

I wouldn't support your conclusion. There's far more relevant things that differentiate "reference"s and "pointer"s. Also, note that (probably unlike C++ pointers, but I'm not sure), Rust references support comparison e.g. of &i32 with &i32. (And this comparison operation will not compare addresses but values!)

Edit: Actually, if you mean "reference" in the C++ kind of meaning of the word, then no, Rust doesn't have anything quite like C++ references.

In Rust terminology, "pointer" is often a general term, describing something that is represented as a memory address internally; so you'd say that a reference (like type &i32) is a pointer, and a raw pointer (like type *const i32) is a pointer, too.

2 Likes

I would like to note that being able to compare two references (with the semantics of comparing the values) doesn't seem to be inherent to Rust. If I understand it right, it's due to an implementation in the standard library (see here).

1 Like

I don't have an answer for why this particular case doesn't work automatically. And in fact, the documentation is incorrect, as this works just fine due to method resolution:

let is_equal = val.eq(val_ref);

So the actual desugaring must not involve method resolution.

That means it's inherent to the == operator, pretty much... unless a definition that doesn't only defer to the trait is stabilized.

1 Like

More reading on the topic here, but I haven't yet read it myself.

Edit: #44762 is the more pertinent one; there's a somewhat low-level overview of how operator lookup (vs. method lookup) works (or worked in 2017 anyway). Looks like it would need a fresh champion to go anywhere. I filed an issue for the PartialEq documentation.

That's correct. It's more like PartialEq::eq(a, b), the same way that for loops desugar to IntoIterator::into_iter(foo), not foo.into_iter().

(This is most relevant in that inherent methods won't get picked up.)

2 Likes

After reading that tracking issue/RFC, it's clear there is no desugaring per se; operator resolution is just its own (not-really-documented) thing. And probably has to remain it's own thing (but being specified eventually would be nice).

Here's an example of == being more coercive than the (simple) fully qualified form. The commented lines don't compile.

fn main() {
    let v = 1;
    let x: *const u32 = &v;
    let y: &u32 = &1;
    let _ = dbg!(x == y);
    // let _ = dbg!(PartialEq::eq(x, y));
    // let _ = dbg!(PartialEq::eq(x, &y));
    // let _ = dbg!(PartialEq::eq(&x, y));
    // let _ = dbg!(PartialEq::eq(&x, &y));
    let _ = dbg!(PartialEq::eq(&x, &(y as _)));
}

(And the last line is too general for other situations.)

1 Like

Thanks for filing the issue! Though, if I understand the posted trackers correctly, this behaviour is likely not going to change? That is, we have to continue to write val == *val_ref? :frowning:

It's very unlikely to change any time soon. There's an interest but it needs a champion to carry it through; it will take some time even once a champion shows up, and there may be roadblocks during the process to boot.

(Lots of initiatives to expand inference stumble because giving the compiler ways to discover more possibilities in one place often results in the compiler having too many possibilities in other places, making inference fail due to ambiguity where it previously succeeded. Which is a long way of saying, changing inference without breaking anything is very tricky.)

In practice I've just gotten used to throwing .copied() into my iterator chains, or binding |&x| in certain closures, or yes just explicitly dereferencing where appropriate. I don't even really think about it; this thread was a reminder that things could be better.

1 Like

That's basically what I was wondering. Is having to write val == *val_ref or &val == val_ref the intended way to write Rust, or am I missing anything? And I guess the answer is: I am not missing anything, as this is just the way Rust's syntax works?

This is just the way Rust's syntax works and I don't think you're missing anything. Adding references or dereferences where needed for the operators is idiomatic.

(Given the tracking issue/RFC, and given that PartialEq is implemented to see through references and compare underlying values, I feel there's a desire to have things work without explicit dereferencing, etc. I don't think intention has much to do with the current state of things; operator resolution was programmed a certain way, Rust grew a lot, and now it's going to be a challenge to "fix" it in a backwards-compatible fashion. Until that day, it's just a papercut to live with.)

I would say it's the way Rust's syntax works (for now).

Sometimes you can work around that by putting the & in a nice place. Consider this example:

fn main() {
    let bools = vec![false, false, true];
    println!(
        "{:?}",
        bools
            .iter()
            .map(|&b| {
                match b { false => "Nein", true => "Ja"}
            })
            .collect::<Vec<_>>()
    )
}

(Playground)

Output:

["Nein", "Nein", "Ja"]

Even if the .map method only gives references, I can write |&b| to make b a bool instead of &bool. This won't make sense when the type iterated over isn't Copy, but sometimes can make the code nicer to read, since you have

match b { false => "Nein", true => "Ja"}

without any & or *, but only an & after map.

I stumbled upon mismatches between references and the referenced values a lot of time in Rust. The tricky part is: In many cases, such as method calls (in regard to the receiver, not the arguments), the Rust compiler will perform the necessary referencing/dereferencing for you automatically. And then you get accustomed to not caring about it. But sometimes (actually in many cases), the compiler won't. And then you get an error.

I got adjusted to always keep in mind whether I'm dealing with references or values. Maybe there is even a pro-side to having the compiler complain about it: you compare two things that aren't even of the same type (bool with &bool), so maybe you did something unintended. It might be good to get an error here (but I'm not sure about it really).

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.