You would not want the language to automatically rewrite a != b
as !(a == b)
when a == b
returns something other than a bool
. And there are a few reasons why you might make it do that.
You may have expression builder objects, where a == b
doesn't and isn't intended to perform any comparison, but simply builds some expression node representing a == b
.
You may have lazy evaluation, where a == b
doesn't and isn't intended to perform any comparison directly, but instead returns some kind of lazy<bool>
that can be converted to bool
implicitly or explicitly at some later time to actually perform the comparison. Possibly combined with the expression builder objects to allow complete expression optimisation before evaluation.
You may have some custom optional<T>
template class, where given optional variables t
and u
, you want to allow t == u
, but make it return optional<bool>
.
There's probably more that I didn't think of. And even though in these examples the operation a == b
and a != b
do both make sense, still a != b
isn't the same thing as !(a == b)
, so separate definitions are needed.
If there is no such possibility, then why on Earth does C++ have these two operators being defined as two distinct functions?
Because you can overload them, and by overloading them you can give them a totally different meaning from their original one.
Take, for example, operator <<
, originally the bitwise left shift operator, now commonly overloaded as an insertion operator, like in std::cout << something
; totally different meaning from the original one.
So, if you accept that the meaning of an operator changes when you overload it, then there is no reason to prevent user from giving a meaning to operator ==
that is not exactly the negation of operator !=
, though this might be confusing.
My concern is, though, why are there two separate definitions needed?
You don't have to define both.
If they are mutually exclusive, you can still be concise by only defining ==
and <
alongside std::rel_ops
Fom cppreference:
#include <iostream>
#include <utility>
struct Foo {
int n;
};
bool operator==(const Foo& lhs, const Foo& rhs)
{
return lhs.n == rhs.n;
}
bool operator<(const Foo& lhs, const Foo& rhs)
{
return lhs.n < rhs.n;
}
int main()
{
Foo f1 = {1};
Foo f2 = {2};
using namespace std::rel_ops;
//all work as you would expect
std::cout << "not equal: : " << (f1 != f2) << '\n';
std::cout << "greater: : " << (f1 > f2) << '\n';
std::cout << "less equal: : " << (f1 <= f2) << '\n';
std::cout << "greater equal: : " << (f1 >= f2) << '\n';
}
Is there any situation possible in which asking questions about two objects being equal does make sense, but asking about them not being equal doesn't make sense?
We often associate these operators to equality.
Although that is how they behave on fundamental types, there is no obligation that this be their behaviour on custom data types.
You don't even have to return a bool if you don't want to.
I've seen people overload operators in bizarre ways, only to find that it makes sense for their domain specific application. Even if the interface appears to show that they are mutually exclusive, the author may want to add specific internal logic.
(either from the user's perspective, or the implementer's perspective)
I know you want a specific example,
so here is one from the Catch testing framework that I thought was practical:
template<typename RhsT>
ResultBuilder& operator == ( RhsT const& rhs ) {
return captureExpression<Internal::IsEqualTo>( rhs );
}
template<typename RhsT>
ResultBuilder& operator != ( RhsT const& rhs ) {
return captureExpression<Internal::IsNotEqualTo>( rhs );
}
These operators are doing different things, and it would not make sense to define one method as a !(not) of the other. The reason this is done, is so that the framework can print out the comparison made. In order to do that, it needs to capture the context of what overloaded operator was used.
There are some very well-established conventions in which (a == b)
and (a != b)
are both false not necessarily opposites. In particular, in SQL, any comparison to NULL yields NULL, not true or false.
It's probably not a good idea to create new examples of this if at all possible, because it's so unintuitive, but if you're trying to model an existing convention, it's nice to have the option to make your operators behave "correctly" for that context.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With