c++c++20spaceship-operator

Spaceship comparison contradicts equality operator


I am defining a Point class where the "size" of a point is defined by its distance from the origin. I.e., a point p1 is "larger" than a point p2 if the sum of the squares of the coordinates of p1 is larger.

Two points have the same "size" if they have the same distance from the origin (e.g., [1,2] and [-1,-2], but they are only "equal" if their coordinates are all the same (e.g. [1,2] and [1,2]).

I thought I'd capture these semantics with the following class definition:

#include <iostream>

class Point {
    private:
    int _x;
    int _y;

    public:
    Point(int x, int y): _x{x}, _y{y} {}

    bool operator==(const Point& p) const = default;

    auto operator<=>(const Point& p) const {
        return _x * _x + _y * _y <=> p._x * p._x + p._y * p._y;
    }
};

But now I'm left with a conundrum: the return type for <=> is deduced as std::strong_ordering, because we are comparing two integers. So different points with the same distance from the origin would be classed as std::strong_ordering::equal:

int main () {
    Point p1{1,2};
    Point p2{-1,-2};

    std::cout << std::is_same_v<decltype(p1 <=> p2), std::strong_ordering> << std::endl; // true
    std::cout << (p1 <=> p2 == std::strong_ordering::equal) << std::endl; // true
    std::cout << (p1 == p2) << std::endl; // false

    return 0;
}

Sure, I could declare the <=> return type to be std::weak_ordering:

std::weak_ordering operator<=>(const Point& p) const { /* etc */ }

But I can't help thinking that I must be missing something. Are there any tools that can help the compiler recognise that the ordering shouldn't be strong in this case? Or is it my responsibility as the designer of class Point to ensure that operators <=> and == are in agreement?

Or still, am I misunderstanding what std::strong_ordering is meant to be? Here I was assuming that two objects should satisfy p1 <=> p2 == std::strong_ordering::equal, if and only if they also satisfy p1 == p2. Perhaps this assumption is flawed.


Solution

  • Ordering and equality comparisons should be consistent. If two instances are not equal under ==, then one should be less than the other. If you build a type where this is not the case, then passing it to any interface that expects strict weak ordering (or basically any rational behavior with regard to comparison) is not going to work.

    Neither strong_ordering nor weak_ordering is appropriate for an inconsistent type. Even partial_ordering is inappropriate. In order for your type to be correctly partially ordered, any two points that are not equal but have the same distance would need to return unordered. Which kind of defeats the point of what you're trying to do.

    Whether it's reasonable to want to compare two "points" to see if one is "less than" the other, if that comparison isn't doing the same thing as equality testing, then it shouldn't use the <=> operator. Just make a non-member named function that makes it clear what question is being asked.