# Boost :

From: Gavin Lambert (boost_at_[hidden])
Date: 2023-02-20 05:12:58

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<int32_t> vs. Point<int64_t>).

Where this doesn't hold, there probably shouldn't be an implementation
of operator== between those types in the first place.

> 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.