ChatGPT解决这个技术问题 Extra ChatGPT

Are == and != mutually dependent?

I'm learning about operator overloading in C++, and I see that == and != are simply some special functions which can be customized for user-defined types. My concern is, though, why are there two separate definitions needed? I thought that if a == b is true, then a != b is automatically false, and vice versa, and there is no other possibility, because, by definition, a != b is !(a == b). And I couldn't imagine any situation in which this wasn't true. But perhaps my imagination is limited or I am ignorant of something?

I know that I can define one in terms of the other, but this is not what I'm asking about. I'm also not asking about the distinction between comparing objects by value or by identity. Or whether two objects could be equal and non-equal at the same time (this is definitely not an option! these things are mutually exclusive). What I'm asking about is this:

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? (either from the user's perspective, or the implementer's perspective)

If there is no such possibility, then why on Earth does C++ have these two operators being defined as two distinct functions?

Two pointers may both be null but not necessarily equal.
Not sure if it makes sense here, but reading this made me think of 'short circuit' issues. For example, one could define that 'undefined' != expression is always true (or false, or undefined), regardless of whether expression can be evaluated. In this case a!=b would return the correct result as per definition, but !(a==b) would fail if b cannot be evaluated. (Or take a lot of time if evaluating b is expensive).
What about null != null and null == null? It can be both... so if a != b that doesn't always mean a == b.
An example from javascript (NaN != NaN) == true

佚名

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.


Expression building is a fantastic practical example of when you'd want this, which doesn't rely on contrived scenarios.
Another good example would be vector logical operations. You'd rather one pass through the data computing != instead of two passes computing == then !. Especially back in the day where you couldn't rely on the compiler to fuse the loops. Or even today if you fail to convince the compiler your vectors don't overlap.
"You may have expression builder objects" -- well then operator ! can also build some expression node and we're still fine replacing a != b with !(a == b), so far as that goes. Same goes for lazy<bool>::operator!, it can return lazy<bool>. optional<bool> is more convincing, since the logical truthiness of for example boost::optional depends on whether a value exists, not on the value itself.
All that, and Nans - please remember the NaNs;
@jsbueno: it's been pointed out further down that NaNs aren't special in this regard.
J
John Kugelman

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.


This is the only answer that makes practical sense.
To me it seems like you have the cause and effect backwards. You can overload them separately because == and != exist as distinct operators. On the other hand, they probably don't exist as distinct operators because you can overload them separately, but due to legacy and convenience (code brevity) reasons.
T
Trevor Hickey

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.


Oh my, how could I not know about std::rel_ops? Thank you a lot for pointing that out.
Near-verbatim copies from cppreference (or anywhere else) should be clearly marked and properly attributed. rel_ops is horrible anyway.
@T.C. Agreed, I'm just saying its a method OP can take. I don't know how to explain rel_ops any simpler than the example shown. I linked to where it is, but posted code since the reference page could always change.
You still need to make it clear that the code example is 99% from cppreference, rather than your own.
Std::relops seems to have fallen out of favor. Check out boost ops for something more targeted.
J
Jander

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.


Implementing SQL-like null behavior in C++? Ewwww. But I suppose it is not something I think should be banned in the language, however distasteful it might be.
@dan1111 More importantly, some flavors of SQL may well be coded in c++, so the language needs to support their syntax, no?
Correct me if I'm wrong, I'm just going off of wikipedia here, but doesn't comparison with a NULL value in SQL return Unknown, not False? And isn't the negation of Unknown still Unknown? So if SQL logic was coded in C++, wouldn't you want NULL == something to return Unknown, and you'd also want NULL != something to return Unknown, and you'd want !Unknown to return Unknown. And in that case implementing operator!= as the negation of operator== is still correct.
@Barmar: Well no, that's not the point. The OP already knows that fact, or this question wouldn't exist. The point was to present an example where it made sense either 1) to implement one of operator== or operator!=, but not the other, or 2) to implement operator!= in a way other than the negation of operator==. And implementing SQL logic for NULL values is not a case of that.
@dan1111 in my experience with sql server and bigquery, X == null and X != null most certainly evaluate to null, not false. How can I tell, you may ask? a) these values display as null, not false b) not (X == null) and not (X != null) don't evaluate to true, is a lesson every sql programmer learns at some point.. Indeed, I believe all major sql implementations adhere very closely to (some iteration of) the sql standard.
C
Centril

I will only answer the second part of your question, namely:

If there is no such possibility, then why on Earth does C++ have these two operators being defined as two distinct functions?

One reason why it makes sense to allow the developer to overload both is performance. You might allow optimizations by implementing both == and !=. Then x != y might be cheaper than !(x == y) is. Some compilers may be able to optimize it for you, but perhaps not, especially if you have complex objects with a lot of branching involved.

Even in Haskell, where developers take laws and mathematical concepts very seriously, one is still allowed to overload both == and /=, as you can see here (http://hackage.haskell.org/package/base-4.9.0.0/docs/Prelude.html#v:-61--61-):

$ ghci
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
λ> :i Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
        -- Defined in `GHC.Classes'

This would probably be considered micro-optimization, but it might be warranted for some cases.


SSE (x86 SIMD) wrapper classes are a great example of this. There's a pcmpeqb instruction, but no packed-compare instruction producing a != mask. So if you can't just reverse the logic of whatever uses the results, you have to use another instruction to invert it. (Fun fact: AMD's XOP instruction-set does have packed-compare for neq. Too bad Intel didn't adopt/extend XOP; there are some useful instructions in that soon-to-be-dead ISA extension.)
The whole point of SIMD in the first place is performance, and you typically only bother to use it manually in loops that are important for overall perf. Saving a single instruction (PXOR with all-ones to invert the compare mask result) in a tight loop can matter.
Performance as a reason is not credible when the overhead is one logical negation.
It might be more than one logical negation if computing x == y costs more significantly more than x != y. Computing the latter might be significantly cheaper due to branch prediction, etc.
B
Benjamin Lindley

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? (either from the user's perspective, or the implementer's perspective)

That's an opinion. Maybe it doesn't. But the language designers, not being omniscient, decided not to restrict people who might come up with situations in which it might make sense (at least to them).


N
Niall

In response to the edit;

That is, if it is possible for some type to have the operator == but not the !=, or vice versa, and when does it make sense to do so.

In general, no, it doesn't make sense. Equality and relational operators generally come in sets. If there is the equality, then the inequality as well; less than, then greater than and so on with the <= etc. A similar approach is applied to the arithmetic operators as well, they also generally come in natural logical sets.

This is evidenced in the std::rel_ops namespace. If you implement the equality and less than operators, using that namespace gives you the others, implemented in terms of your original implemented operators.

That all said, are there conditions or situations where the one would not immediately mean the other, or could not be implemented in terms of the others? Yes there are, arguably few, but they are there; again, as evidenced in the rel_ops being a namespace of its own. For that reason, allowing them to be implemented independently allows you to leverage the language to get the semantics you require or need in a way that is still natural and intuitive for the user or client of the code.

The lazy evaluation already mentioned is an excellent example of this. Another good example is giving them semantics that don't mean equality or in-equality at all. A similar example to this is the bit shift operators << and >> being used for stream insertion and extraction. Although it may be frowned upon in general circles, in some domain specific areas it may make sense.


T
Taywee

If the == and != operators don't actually imply equality, in the same way that the << and >> stream operators don't imply bit-shifting. If you treat the symbols as if they mean some other concept, they don't have to be mutually exclusive.

In terms of equality, it could make sense if your use-case warrants treating objects as non-comparable, so that every comparison should return false (or a non-comparable result type, if your operators return non-bool). I can't think of a specific situation where this would be warranted, but I could see it being reasonable enough.


d
dippas

With great power comes great responsibly, or at least really good style guides.

== and != can be overloaded to do whatever the heck you want. It's both a blessing and a curse. There's no guarantee that != means !(a==b).


D
Dafang Cao
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

BoolPlus operator==(File& other);
BoolPlus operator!=(File& other);

I can't justify this operator overloading, but in the example above it is impossible to define operator!= as the "opposite" of operator==.


@Snowman: Dafang doesn't say its a good enumeration (nor a good idea to define an enumeration like that), it is just an example to illustrate a point. With this (perhaps bad) operator definition, then != would indeed not mean the opposite of ==.
@AlainD did you click the link I posted, and are you aware of the purpose of that site? This is called "humor."
@Snowman: I certainly do...sorry, I missed it was a link and intended as irony! :o)
A
Azeem

In the end, what you are checking with those operators is that the expression a == b or a != b is returning a Boolean value (true or false). These expression returns a Boolean value after comparison rather than being mutually exclusive.


D
Daniel Jour

[..] why are there two separate definitions needed?

One thing to consider is that there might be the possibility of implementing one of these operators more efficiently than just using the negation of the other.

(My example here was rubbish, but the point still stands, think of bloom filters, for example: They allow fast testing if something is not in a set, but testing if it's in may take a lot more time.)

[..] by definition, a != b is !(a == b).

And it's your responsibility as programmer to make that hold. Probably a good thing to write a test for.


How does !((a == rhs.a) && (b == rhs.b)) not allow short circuiting? if !(a == rhs.a), then (b == rhs.b) will not be evaluated.
This is a bad example, though. The short-circuiting adds no magical advantage here.
@Oliver Charlesworth Alone it doesn't, but when joined with separate operators, it does: In case of ==, it will stop comparing as soon as the first corresponding elements are non-equal. But in case of !=, if it were implemented in terms of ==, it would need to compare all the corresponding elements first (when they're all equal) to be able to tell that they are not non-equal :P But when implemented as in the example above, it will stop comparing as soon as it finds the first non-equal pair. Great example indeed.
@BenjaminLindley True, my example was complete nonsense. Unfortunately, I cannot come up with another atm, it's too late here.
@BarbaraKwarc: !((a == b) && (c == d)) and (a != b) || (c != d) are equivalent in terms of short-circuiting efficiency.
T
TOOGAM

By customizing the behavior of the operators, you can make them do what you want.

You may wish to customize things. For instance, you may wish to customize a class. Objects of this class can be compared just by checking a specific property. Knowing that this is the case, you can write some specific code that only checks the minimum things, instead of checking every single bit of every single property in the whole object.

Imagine a case where you can figure out that something is different just as fast, if not faster, than you can find out something is the same. Granted, once you figure out whether something is the same or different, then you can know the opposite simply by flipping a bit. However, flipping that bit is an extra operation. In some cases, when code gets re-executed a lot, saving one operation (multiplied by many times) can have an overall speed increase. (For instance, if you save one operation per pixel of a megapixel screen, then you've just saved a million operations. Multiplied by 60 screens per second, and you save even more operations.)

hvd's answer provides some additional examples.


o
oliora

Yes, because one means "equivalent" and another means "non-equivalent" and this terms are mutually exclusive. Any other meaning for this operators is confusing and should be avoided by all means.


They are not mutually exclusive for all cases. For example, two infinities both not equal to each other and not not equal to each other.
@vladon can use use one instead other in generic case? No. This means they are just not equal. All the rest goes to a special function rather than to operator==/!=
@vladon please, instead of generic case read all cases in my answer.
@vladon As much as this is true in maths, can you give an example where a != b is not equal to !(a == b) for this reason in C?
A
Azeem

Maybe an uncomparable rule, where a != b was false and a == b was false like a stateless bit.

if( !(a == b || a != b) ){
    // Stateless
}

If you want to rearrange logical symbols then !( [A] || [B]) logically becomes ([!A]&[!B])
Note that the return type of operator==() and operator!=() are not necessarily bool, they might be an enum that include stateless if you wanted that and yet the operators might still be defined so (a != b) == !(a==b) holds..