non-defaulted operator <=> doesn't generate == and != in C++20 - c++

I'm running into a strange behavior with the new spaceship operator <=> in C++20. I'm using Visual Studio 2019 compiler with /std:c++latest.
This code compiles fine, as expected:
#include <compare>
struct X
{
int Dummy = 0;
auto operator<=>(const X&) const = default; // Default implementation
};
int main()
{
X a, b;
a == b; // OK!
return 0;
}
However, if I change X to this:
struct X
{
int Dummy = 0;
auto operator<=>(const X& other) const
{
return Dummy <=> other.Dummy;
}
};
I get the following compiler error:
error C2676: binary '==': 'X' does not define this operator or a conversion to a type acceptable to the predefined operator
I tried this on clang as well, and I get similar behavior.
I would appreciate some explanation on why the default implementation generates operator== correctly, but the custom one doesn't.

This is by design.
[class.compare.default] (emphasis mine)
3 If the class definition does not explicitly declare an ==
operator function, but declares a defaulted three-way comparison
operator function, an == operator function is declared implicitly
with the same access as the three-way comparison operator function.
The implicitly-declared == operator for a class X is an inline
member and is defined as defaulted in the definition of X.
Only a defaulted <=> allows a synthesized == to exist. The rationale is that classes like std::vector should not use a non-defaulted <=> for equality tests. Using <=> for == is not the most efficient way to compare vectors. <=> must give the exact ordering, whereas == may bail early by comparing sizes first.
If a class does something special in its three-way comparison, it will likely need to do something special in its ==. Thus, instead of generating a potentially non-sensible default, the language leaves it up to the programmer.

During the standardization of this feature, it was decided that equality and ordering should logically be separated. As such, uses of equality testing (== and !=) will never invoke operator<=>. However, it was still seen as useful to be able to default both of them with a single declaration. So if you default operator<=>, it was decided that you also meant to default operator== (unless you define it later or had defined it earlier).
As to why this decision was made, the basic reasoning goes like this. Consider std::string. Ordering of two strings is lexicographical; each character has its integer value compared against each character in the other string. The first inequality results in the result of ordering.
However, equality testing of strings has a short-circuit. If the two strings aren't of equal length, then there's no point in doing character-wise comparison at all; they aren't equal. So if someone is doing equality testing, you don't want to do it long-form if you can short-circuit it.
It turns out that many types that need a user-defined ordering will also offer some short-circuit mechanism for equality testing. To prevent people from implementing only operator<=> and throwing away potential performance, we effectively force everyone to do both.

The other answers explain really well why the language is like this. I just wanted to add that in case it's not obvious, it is of course possible to have a user-provided operator<=> with a defaulted operator==. You just need to explicitly write the defaulted operator==:
struct X
{
int Dummy = 0;
auto operator<=>(const X& other) const
{
return Dummy <=> other.Dummy;
}
bool operator==(const X& other) const = default;
};
Note that the defaulted operator== performs memberwise == comparisons. That is to say, it is not implemented in terms of the user-provided operator<=>. So requiring the programmer to explicitly ask for this is a minor safety feature to help prevent surprises.

Related

Why doesn't the spaceship operator allow me to perform an equality operation but allow inequality operations? [duplicate]

I'm running into a strange behavior with the new spaceship operator <=> in C++20. I'm using Visual Studio 2019 compiler with /std:c++latest.
This code compiles fine, as expected:
#include <compare>
struct X
{
int Dummy = 0;
auto operator<=>(const X&) const = default; // Default implementation
};
int main()
{
X a, b;
a == b; // OK!
return 0;
}
However, if I change X to this:
struct X
{
int Dummy = 0;
auto operator<=>(const X& other) const
{
return Dummy <=> other.Dummy;
}
};
I get the following compiler error:
error C2676: binary '==': 'X' does not define this operator or a conversion to a type acceptable to the predefined operator
I tried this on clang as well, and I get similar behavior.
I would appreciate some explanation on why the default implementation generates operator== correctly, but the custom one doesn't.
This is by design.
[class.compare.default] (emphasis mine)
3 If the class definition does not explicitly declare an ==
operator function, but declares a defaulted three-way comparison
operator function, an == operator function is declared implicitly
with the same access as the three-way comparison operator function.
The implicitly-declared == operator for a class X is an inline
member and is defined as defaulted in the definition of X.
Only a defaulted <=> allows a synthesized == to exist. The rationale is that classes like std::vector should not use a non-defaulted <=> for equality tests. Using <=> for == is not the most efficient way to compare vectors. <=> must give the exact ordering, whereas == may bail early by comparing sizes first.
If a class does something special in its three-way comparison, it will likely need to do something special in its ==. Thus, instead of generating a potentially non-sensible default, the language leaves it up to the programmer.
During the standardization of this feature, it was decided that equality and ordering should logically be separated. As such, uses of equality testing (== and !=) will never invoke operator<=>. However, it was still seen as useful to be able to default both of them with a single declaration. So if you default operator<=>, it was decided that you also meant to default operator== (unless you define it later or had defined it earlier).
As to why this decision was made, the basic reasoning goes like this. Consider std::string. Ordering of two strings is lexicographical; each character has its integer value compared against each character in the other string. The first inequality results in the result of ordering.
However, equality testing of strings has a short-circuit. If the two strings aren't of equal length, then there's no point in doing character-wise comparison at all; they aren't equal. So if someone is doing equality testing, you don't want to do it long-form if you can short-circuit it.
It turns out that many types that need a user-defined ordering will also offer some short-circuit mechanism for equality testing. To prevent people from implementing only operator<=> and throwing away potential performance, we effectively force everyone to do both.
The other answers explain really well why the language is like this. I just wanted to add that in case it's not obvious, it is of course possible to have a user-provided operator<=> with a defaulted operator==. You just need to explicitly write the defaulted operator==:
struct X
{
int Dummy = 0;
auto operator<=>(const X& other) const
{
return Dummy <=> other.Dummy;
}
bool operator==(const X& other) const = default;
};
Note that the defaulted operator== performs memberwise == comparisons. That is to say, it is not implemented in terms of the user-provided operator<=>. So requiring the programmer to explicitly ask for this is a minor safety feature to help prevent surprises.

How to write custom spaceship operator in c++20 [duplicate]

I'm running into a strange behavior with the new spaceship operator <=> in C++20. I'm using Visual Studio 2019 compiler with /std:c++latest.
This code compiles fine, as expected:
#include <compare>
struct X
{
int Dummy = 0;
auto operator<=>(const X&) const = default; // Default implementation
};
int main()
{
X a, b;
a == b; // OK!
return 0;
}
However, if I change X to this:
struct X
{
int Dummy = 0;
auto operator<=>(const X& other) const
{
return Dummy <=> other.Dummy;
}
};
I get the following compiler error:
error C2676: binary '==': 'X' does not define this operator or a conversion to a type acceptable to the predefined operator
I tried this on clang as well, and I get similar behavior.
I would appreciate some explanation on why the default implementation generates operator== correctly, but the custom one doesn't.
This is by design.
[class.compare.default] (emphasis mine)
3 If the class definition does not explicitly declare an ==
operator function, but declares a defaulted three-way comparison
operator function, an == operator function is declared implicitly
with the same access as the three-way comparison operator function.
The implicitly-declared == operator for a class X is an inline
member and is defined as defaulted in the definition of X.
Only a defaulted <=> allows a synthesized == to exist. The rationale is that classes like std::vector should not use a non-defaulted <=> for equality tests. Using <=> for == is not the most efficient way to compare vectors. <=> must give the exact ordering, whereas == may bail early by comparing sizes first.
If a class does something special in its three-way comparison, it will likely need to do something special in its ==. Thus, instead of generating a potentially non-sensible default, the language leaves it up to the programmer.
During the standardization of this feature, it was decided that equality and ordering should logically be separated. As such, uses of equality testing (== and !=) will never invoke operator<=>. However, it was still seen as useful to be able to default both of them with a single declaration. So if you default operator<=>, it was decided that you also meant to default operator== (unless you define it later or had defined it earlier).
As to why this decision was made, the basic reasoning goes like this. Consider std::string. Ordering of two strings is lexicographical; each character has its integer value compared against each character in the other string. The first inequality results in the result of ordering.
However, equality testing of strings has a short-circuit. If the two strings aren't of equal length, then there's no point in doing character-wise comparison at all; they aren't equal. So if someone is doing equality testing, you don't want to do it long-form if you can short-circuit it.
It turns out that many types that need a user-defined ordering will also offer some short-circuit mechanism for equality testing. To prevent people from implementing only operator<=> and throwing away potential performance, we effectively force everyone to do both.
The other answers explain really well why the language is like this. I just wanted to add that in case it's not obvious, it is of course possible to have a user-provided operator<=> with a defaulted operator==. You just need to explicitly write the defaulted operator==:
struct X
{
int Dummy = 0;
auto operator<=>(const X& other) const
{
return Dummy <=> other.Dummy;
}
bool operator==(const X& other) const = default;
};
Note that the defaulted operator== performs memberwise == comparisons. That is to say, it is not implemented in terms of the user-provided operator<=>. So requiring the programmer to explicitly ask for this is a minor safety feature to help prevent surprises.

What are the breaking changes caused by rewritten comparison operators?

There are some new rules about rewritten comparison operators in C++20, and I'm trying to understand how they work. I've run into the following program:
struct B {};
struct A
{
bool operator==(B const&); // #1
};
bool operator==(B const&, A const&); // #2
int main()
{
B{} == A{}; // C++17: calls #2
// C++20: calls #1
}
which actually breaks existing code. I'm a little surprised by this; #2 actually still looks better to me :p
So how do these new rules change the meaning of existing code?
That particular aspect is a simple form of rewriting, reversing the operands. The primary operators == and <=> can be reversed, the secondaries !=, <, >, <=, and >=, can be rewritten in terms of the primaries.
The reversing aspect can be illustrated with a relatively simple example.
If you don't have a specific B::operator==(A) to handle b == a, you can use the reverse to do it instead: A::operator==(B). This makes sense because equality is a bi-directional relationship: (a == b) => (b == a).
Rewriting for secondary operators, on the other hand, involves using different operators. Consider a > b. If you cannot locate a function to do that directly, such as A::operator>(B), the language will go looking for things like A::operator<=>(B) then simply calculating the result from that.
That's a simplistic view of the process but it's one that most of my students seem to understand. If you want more details, it's covered in the [over.match.oper] section of C++20, part of overload resolution (# is a placeholder for the operator):
For the relational and equality operators, the rewritten candidates include all member, non-member, and built-in candidates for the operator <=> for which the rewritten expression (x <=> y) # 0 is well-formed using that operator<=>.
For the relational, equality, and three-way comparison operators, the rewritten candidates also include a synthesized candidate, with the order of the two parameters reversed, for each member, non-member, and built-in candidate for the
operator <=> for which the rewritten expression 0 # (y <=> x) is well-formed using that operator<=>.
Hence gone are the days of having to provide a real operator== and operator<, then boiler-plating:
operator!= as ! operator==
operator> as ! (operator== || operator<)
operator<= as operator== || operator<
operator>= as ! operator<
Don't complain if I've gotten one or more of those wrong, that just illustrates my point on how much better C++20 is, since you now only have to provide a minimal set (most likely just operator<=> plus whatever else you want for efficiency) and let the compiler look after it :-)
The question as to why one is being selected over the other can be discerned with this code:
#include <iostream>
struct B {};
struct A {
bool operator==(B const&) { std::cout << "1\n"; return true; }
};
bool operator==(B const&, A const&) { std::cout << "2\n"; return true; }
int main() {
auto b = B{}; auto a = A{};
b == a; // outputs: 1
(const B)b == a; // 1
b == (const A)a; // 2
(const B)b == (const A)a; // 2
}
The output of that indicates that it's the const-ness of a deciding which is the better candidate.
As an aside, you may want to have a look at this article, which offers a more in-depth look.
From a non-language-lawyer sense, it works like this. C++20 requires that operator== compute whether the two objects are equal. The concept of equality is commutative: if A == B, then B == A. As such, if there are two operator== functions that could be called by C++20's argument reversal rules, then your code should behave identically either way.
Basically, what C++20 is saying is that if it matters which one gets called, you're defining "equality" incorrectly.
So let's get into the details. And by "the details", I mean the most horrifying chapter of the standard: function overload resolution.
[over.match.oper]/3 defines the mechanism by which the candidate function set for an operator overload is built. C++20 adds to this by introducing "rewritten candidates": a set of candidate functions discovered by rewriting the expression in a way that C++20 deems to be logically equivalent. This only applies to the relational and in/equality operators.
The set is built in accord with the following:
For the relational ([expr.rel]) operators, the rewritten candidates include all non-rewritten candidates for the expression x <=> y.
For the relational ([expr.rel]) and three-way comparison ([expr.spaceship]) operators, the rewritten candidates also include a synthesized candidate, with the order of the two parameters reversed, for each non-rewritten candidate for the expression y <=> x.
For the != operator ([expr.eq]), the rewritten candidates include all non-rewritten candidates for the expression x == y.
For the equality operators, the rewritten candidates also include a synthesized candidate, with the order of the two parameters reversed, for each non-rewritten candidate for the expression y == x.
For all other operators, the rewritten candidate set is empty.
Note the particular concept of a "synthesized candidate". This is standard-speak for "reversing the arguments".
The rest of the section details what it means if one of the rewritten candidates gets chosen (aka: how to synthesize the call). To find which candidate gets chosen, we must delve into the most horrifying part of the most horrifying chapter of the C++ standard:
Best viable function matching.
What matters here is this statement:
a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then
And that matters... because of this. Literally.
By the rules of [over.ics.scs], an identity conversion is a better match than a conversion that adds a qualifier.
A{} is a prvalue, and... it's not const. Neither is the this parameter to the member function. So it's an identity conversion, which is a better conversion sequence than one that goes to the const A& of the non-member function.
Yes, there is a rule further down that explicitly makes rewritten functions in the candidate list less viable. But it doesn't matter, because the rewritten call is a better match on function arguments alone.
If you use explicit variables and declare one like this A const a{};, then [over.match.best]/2.8 gets involved and de-prioritizes the rewritten version. As seen here. Similarly, if you make the member function const, you also get consistent behavior.

C++20 explicitly defaulted equality operator

I'm trying to understand the new default comparison operators introduced in C++20. My issue is about when an explicitly defaulted comparison operator gets implicitly defined. The following code example illustrates the question:
#include <iostream>
struct B
{
operator bool() const { return true; }
};
struct D : B
{
bool operator==(const D&) const = default;
};
bool operator==(B, B) { return false; }
int main ()
{ D d;
std::cout << (d == d);
}
/* Outputs:
0 in gcc 10.1
1 in msvc 19.26
*/
The output of this program is compiler-dependent. It seems that MSVC defines the operator== for class D when it is encounters the declaration as defaulted, hence it doesn't use the operator== that is defined later for class B. By contrast, gcc waits with the implicit definition of D's operator== until it is actually needed, by which time the operator== defined for B is in scope, and gets used. Which behavior, if either, is correct ?
A related question, is why a defaulted operator== can't be declared for a class with reference members ? I could see that reference members could pose a problem with the MSVC approach, because a reference member might refer to an incomplete type when the defaulting declaration for operator== is encountered. With the gcc approach, the reference's type would always be complete before gcc attempts to define the defaulted operator.
GCC is wrong here (not unsurprisingly considering its results). When creating the definition of a defaulted comparison operator, the standard says:
Name lookups in the defaulted definition of a comparison operator function are performed from a context equivalent to its function-body.
And the function body of a defaulted function is where = default is. Therefore, the bool operator==(B, B) function should not be visible to the function body of the defaulted comparison operator.

Prevent converting two objects from two classes to bool when doing : if(a!=b)

I have two classes A and B, each having an operator bool() defined.
I recently came across a bug which was caused by:
A a;
B b;
if(a!=b)
{
//...
}
The code compiled fine under gcc 4.9.1 and implicitly converted a and b to bool before comparison.
Is it possible to define something that would prevent this and cause a compile error, to force the programmer to use an explicit conversion function provided by A and B? Declarations of A and B should be unrelated, they are in to different headers.
The obvious way is to mark operator bool() explicit in either A or B (or both). That will cause a compiler error. Bear in mind it may cause some other usages of A or B - where such an implicit conversion works as intended - to also not compile. There is no free lunch.
The intent of the code is presumably to compare the objects (or their data members in some way). If that is so [that is a design choice for you] it would also be worth supplying a
bool operator!=(const A&, const B&); // usage a != b
bool operator!=(const B&, const A&); // usage b != a
Depending on the types, passing one or more by value may be appropriate. An alternative (e.g. if both types are struct/class types) is to implement bool operator!=(const B &) const as a member of A, and/or bool operator!=(const A &) const as a member of B.
Bear in mind that, if an operator!=() is supplied for two types, it may also be appropriate to provide other comparison operators (such as ==). That is a design decision though.
Another "hack" would be to make the bool conversion explicit as M.M already mentioned, but this brings some other issues with it.
something like this:
class A { // same for class B
public:
explicit operator bool() {
// some code
return (true || false) && !(false && true);
}
};
Now if(a==b) won't compile anymore, but if(a) and if(b) will. HOWEVER, this will lead to some other issues and unexpected behavior like if(a==true) also won't compile anymore, since a is casted to an integer; Which is also the reason why if(a==b) was compiling in the first place.
Another option would be to delete the implicit int conversion: operator int() = delete;
Here is an example you can play arround and see what can happen if you mess with conversion operators...