More surprises with the `==` operator

This actually does not fail. I think I mixed something up. I'm sorry. I had a previous version that looked different when it was failing. Maybe I'll figure out again what confused me.


Okay, the surprising thing is this:

fn main() {
    let array: [u8; 1] = [0u8; 1];
    let array_ref: &[u8; 1] = &[0u8; 1];
    let slice: &[u8] = &vec![0u8];
    assert!(array == slice); // works
    assert!(array == array_ref); // fails
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: can't compare `[u8; 1]` with `&[u8; 1]`
 --> src/main.rs:6:19
  |
6 |     assert!(array == array_ref); // fails
  |                   ^^ no implementation for `[u8; 1] == &[u8; 1]`
  |
  = help: the trait `PartialEq<&[u8; 1]>` is not implemented for `[u8; 1]`
  = help: the following other types implement trait `PartialEq<Rhs>`:
            <&[B] as PartialEq<[A; N]>>
            <&[T] as PartialEq<Vec<U, A>>>
            <&mut [B] as PartialEq<[A; N]>>
            <&mut [T] as PartialEq<Vec<U, A>>>
            <[A; N] as PartialEq<&[B]>>
            <[A; N] as PartialEq<&mut [B]>>
            <[A; N] as PartialEq<[B; N]>>
            <[A; N] as PartialEq<[B]>>
          and 3 others

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` due to previous error

I would expect that if I can't compare [0; 1] with &[0; 1], then I would also not be able to compare [0; 1] with a slice. But comparison with &[u8] works (but not with &[u8; 1]).

That is a bit... strange (but may be just like how the std library works).


It works exactly because of this impl:

So unsized coercion is indeed not involved.

Side question: How can it be existing since Rust 1.0 while it uses const generics? (Might need to look that up in the repository history.)

There used to be a macro that generated the 33 separate impls. It made the docs huge and ugly https://doc.rust-lang.org/1.15.0/std/cmp/trait.PartialEq.html

Those labels of when things were added aren't fully reliable, and don't reflect various important changes (like when no-reallocation guarantees are added). If you really care about what was available in a particular release, the right answer is to look at the docs from that version. (And to build using that version of rust in CI in addition to stable.)

5 Likes

That documentation is counterfactual, and see also #44762.

(I didn't read this thread in depth.)

4 Likes

The analysis above is verbose. Here is a simple workflow:

let array: [u8; 1] = [0u8; 1];
let array_ref: &[u8; 1] = &[0u8; 1];
let slice: &[u8] = &vec![0u8];
assert!(slice == array_ref); // &[u8]: PartialEq<&[u8; 1]> => [u8]: PartialEq<[u8; 1]>
  1. Write down the left-hand-side and right-hand-side types: &[u8] == &[u8; 1]

  2. Turn it into the trait bound form[1]: &[u8]: PartialEq<&[u8; 1]>
    or impl form: impl PartialEq<&[u8; 1]> for &[u8]

  3. search for &[

    impl<A, B, const N: usize> PartialEq<[A; N]> for &[B]
    impl<T, U, A> PartialEq<Vec<U, A>> for &[T]
    

    neither are wanted

  4. search for common generic impls and deduce

    impl<A, B> PartialEq<&B> for &A
    where
        A: PartialEq<B> + ?Sized,
        B: ?Sized,
    

    thus we can remove & on both sides: [u8]: PartialEq<[u8; 1]>
    then we'll find impl<A, B, const N: usize> PartialEq<[A; N]> for [B]


  1. Make sure you get the the correct a == b desugaring:
    <TypeOfA as PartialEq<TypeOfB>>::eq(&a, &b) ↩︎

2 Likes

Yeah, I see. It's pretty "verbose" to find the right implementation. Thank you for clearing this up!

It makes sense there is an impl<A, B, const N: usize> PartialEq<[A; N]> for [B] (which then allows us to compare a &[B] with an &[A; N] through the generic impl for references on both sides.

But the surprising thing (for me) is this impl:

PartialEq<&[B]> for [A; N]

Which allows this:

I find it surprising that I can compare an owned/non-reference array [A; N] with a referenced slice &[B]. But maybe it's included in the standard library because it's something handy and doesn't cause problems with overlapping implementations (as some other impls would do).

I noticed this because sometimes, when I manually do an unsized coercion, code will work that didn't work before. For example:

fn main() {
    let array: [u8; 1] = [0u8; 1];
    let array_ref: &[u8; 1] = &[0u8; 1];
    let slice: &[u8] = &vec![0u8];
    assert!(array == slice); // works
    //assert!(array == array_ref); // fails
    assert!(array == &array_ref[..]); // works again
    assert!(array == array_ref as &[_]); // different syntax, also works
}

(Playground)

I believe the reason for not allowing different levels of referencing on a single side of the == operator is due to problems with Rust's type system (overlapping implementatins), and if I understand it right, implementing it for a single case (such as for arrays and referenced slices) causes no problem. Still leaves one with surprises when comparison (or other operations) with different reference levels will work or fail.

But you might be unsurprised at String: PartialEq<&str> and surprised at String: PartialEq<str>:

let s = String::new();
dbg!(s == ""); // works: unsurprised
dbg!(s == *""); // works: surprised

Update: I think it's natural to use the second impl when you have &String == &str:

dbg!(&s == ""); // works

So I suppose the main reason for supporting these symmetric impls

impl<A, B, const N: usize> PartialEq<[A; N]> for &[B]
impl<A, B, const N: usize> PartialEq<&[B]> for [A; N]

impl<T, U, A> PartialEq<Vec<U, A>> for &[T]
impl<T, U, A> PartialEq<&[U]> for Vec<T, A>

impl<'a, 'b> PartialEq<&'a str> for String
impl<'a, 'b> PartialEq<String> for &'a str

is &str and &[T] are so common that you hardly notice that you're comparing an owned type with a referenced type.

let slice: &[u8] = &[1]; // comes from a fn argument or generic as_ref or coercion or whatever
assert_eq!([1], slice); // [1] or vec![1] as an owned type comes from the result of a function/method or a locally initialised variable
assert_eq!(vec![1], slice);
3 Likes

Kind of unsurprised, as I use it a lot. Though now that I think of it, Rust could also require to write &owned_string == string_slice.

But we have also:

fn main() {
    let vec: Vec<u8> = b"binstr".to_vec();
    let slice: &[u8] = &vec;
    assert_eq!(vec, slice);
}

(Playground)

So Rust is consistent here between Vec<T> and String.

I guess it makes sense to implement PartialEq<B> for O where O: Borrow<B>, but I also guess such a blanket implementation would not work due to potentially overlapping implementations (not sure though). Update: See also Playground. Borrow<T> is implemented for every T: ?Sized, so this could not work (with current Rust).

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.