I would argue that in C++20, comparisons should be member functions unless you have a strongly compelling reason otherwise.
Lemme first start with the C++17 calculus: we would often write our comparisons as non-members. The reason for this is that it was the only way to allow two-sided comparisons. If I had a type X
that I wanted to be comparable to int
, I can't make 1 == X{}
work with a member function - it has to be a free function:
struct X { };
bool operator==(X, int);
bool operator==(int lhs, X rhs) { return rhs == lhs; }
There wasn't much choice in the matter. Now, writing these as purely free functions is sub-optimal because we're polluting the namespace and increasing the amounts of candidates in lookup - so it's better to make them hidden friends:
struct X {
friend bool operator==(X, int);
friend bool operator==(int lhs, X rhs) { return rhs == lhs; }
};
In C++20, we don't have this issue because the comparisons are themselves symmetric. You can just write:
struct X {
bool operator==(int) const;
};
And that declaration alone already allows both X{} == 1
and 1 == X{}
, while also already not contributing extra candidates for name lookups (it will already only be a candidate if one side or the other is an X
).
Moreover, in C++20, you can default comparisons if they're declared within the declaration of the class. These could be either member functions or hidden friends, but not external free functions.
One interesting case for a reason to provide non-member comparison is what I ran into with std::string
. The comparisons for that type are currently non-member function templates:
template<class charT, class traits, class Allocator>
constexpr bool
operator==(const basic_string<charT, traits, Allocator>& lhs,
const basic_string<charT, traits, Allocator>& rhs) noexcept;
This has importantly different semantics from making this a member (non-template) function or a hidden friend (non-template) function in that it doesn't allow implicit conversions, by way of being a template. As I pointed out, turning this comparison operator into a non-template would have the effect of suddenly allowing implicit conversions on both sides which can break code that wasn't previously aware of this possibility.
But in any case, if you have a class template and want to avoid conversions on your comparisons, that might be a good reason to stick with a non-member function template for your comparison operator. But that's about it.