On 20/02/2023 17:09, Peter Dimov wrote:
It breaks the idea that "==" is an equivalence relation, which seems to me to unnecessarily complicate things for the user.
Heterogeneous comparisons aren't equivalence relations. Values of different types can't be equivalent.
They can't be equal, but they can be equivalent.
A classic example is a type like optional<T>, which by nature can
perfectly represent all possible values of T, so operator==(optional<T>,
T) (and conversely operator==(T, optional<T>)) are well-defined for all
possible values of T. There is one value of optional<T> that cannot
possibly be contained by T, but that's still well-defined, it's just
known to always return false.
You can imagine all manner of other types where this can hold -- any
case where one type is a strict superset of another, such as sum types
or types with equivalent layout but strictly-larger-scale members (like
a Point
The principled approach to heterogeneous comparisons is to define x == y as C(x) == C(y), where C is the common type of X and Y, i.e. a type that can represent all values of X and all values of Y. But this (a) only shifts the question to "who decides C"
A common type does not need to represent all values of both types; only enough to determine equivalence. This usually means the "common type" is the smaller type, because a failure to convert the larger type to the smaller type inherently implies non-equivalence. As for "who decides it", this is usually the larger type, because the larger type is usually defined in terms of the smaller type and not the reverse, as I mentioned in another post.
and (b) doesn't at all work for any op== that doesn't follow the principled approach, such as boost::function::operator== (which considers x == y true when the boost::function x contains y, but for which x == x doesn't compile), or bind(f, _1) == v, which constructs a lambda expression that returns f(x) == v. (Or for _1 == v when using Lambda/Lambda2, for that matter.)
Both `bind(f, _1) == v` and `v == bind(f, _1)` should produce the equivalent lambda. (Not identical, perhaps -- one might `return m_f(m_x) == m_v` and the other might `return m_v == m_f(m_x)` internally, but these should be functionally equivalent. Or perhaps both do indeed return exactly the same implementation, and that's ok too.) If this is true, the commutative property has not been broken and this shouldn't break in C++20 regardless of which way it chooses to evaluate it. (And bonus: both orders will work even if you chose to only implement one of the two.) If that's not true, then I would argue that it's wrong and it should stop using operator== for that purpose. I'm not sure I sufficiently understand the boost::function case to comment on it.
There are tons of existing C++ code that works perfectly well without adhering to principled approaches to defining op==, and breaking this willy-nilly was irresponsible.
I would instead argue that implementing op== in an unprincipled manner was irresponsible in the first place. :) There are a lot more code-sanitizer, linter, and auto-rewriting tools around than in prior days, and these generally benefit from being able to make basic assumptions such as language-specified commutability not being violated. It's also less surprising to users.