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
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.
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.
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.
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...
Suppose I have a class that doesn't support memberwise copying so I don't want to preserve compiler-implemented copy-constructor and assignment operator. I also don't want to implement those because either
doing so takes extra effort and I don't need those operations in my class or
those operations won't make sense in my class
so I want to prohibit them. To do so I'll declare them private and provide no implementation:
class NonCopyable {
private:
NonCopyable( const NonCopyable& ); //not implemented anywhere
void operator=( const NonCopyable& ); //not implemented anywhere
};
Now I can select any return type for operator=() member function. Will it matter which return type I select?
No, the return type doesn't matter.†
The C++ standard does not impose any requirements on the return type for copy assignment special member functions that you declare yourself. It just needs to be an operator=() that acccepts "exactly one parameter of type X, X&, const X&, volatile X& or const volatile X&".†† Therefore, void operator=( const NonCopyable& ); is still a copy assignment operator (a user-declared one, to be specific).
Because you have in fact supplied your own copy assignment operator, it will surpress the generation of the default copy assignment operator. This forces all calls to NonCopyable's copy assignment operator to resolve to yours, causing any attempts to use the copy assignment operator to fail to compile since it's declared private.
class Foo : NonCopyable
{
};
int main()
{
Foo a;
Foo b;
// Compiler complains about `operator=(const NonCopyable&)`
// not accessible or something like that.
a = b;
}
And since I'll never be able to actually use it, it doesn't matter that it's not exactly the canonical copy assignment operator. If I tried to use the copy assignment operator it would result in a compiler error, which is exactly what you want.
† Of course, it does matter, stylistically speaking, if the copy assignment operator actually does something. Generally you want your operators to behave just like the built-in ones, so returning a X& is good practice when you're actually doing assignment.
†† C++ Standard: 12.8 Copying class objects [class.copy]
9 A user-declared copy assignment operator X::operator= is a non-static non-template member function of class X with exactly one
parameter of type X, X&, const X&, volatile X& or const
volatile X&.
No, since you'll never call this operator in your code. I tend to keep the return type NonCopyable&, for clarity and consistency.
It matters a tiny, tiny bit:
void ensures a small percentage of accidental/misguided calls (a = b = c / f(d = e)) from within the class's implementation produce compile time errors rather than link time errors, which may save compile time and be more understandable (minimally relevant for large classes touched by many developers, some with limited prior familiarity).
void would ring an alarm bell for me (and hopefully most developers), wondering whether you:
wanted to remove the default-generated operator=
were just lazy about the extra typing, or
were unfamiliar/uncaring re the generally expected semantics of operator=.
With the open question in mind, other programmer are less likely to come along and think you just didn't get around to providing the implementation and add it casually (you may feel comments are adequate).
returning a reference to type may make the overall function signature more instantaneously recognisable, or visually searching past a complicated type to find operator= could have the opposite effect - all in the eye (and mind) of the beholder....
No, because you can return anything from operator=, even if you define its implementation.
No, it does not matter, since you never implement a return statement. If you try to invoke the operator, the compiler won't be able to find an implementation (with any return type, so the return type is irrelevant).
Interestingly, boost::noncopyable's copy assignment operator is declared to return a const noncopyable&, but I guess that's just convention.