Comparing two instances of the following struct, I receive an error:
struct MyStruct1 {
MyStruct1(const MyStruct2 &_my_struct_2, const int _an_int = -1) :
my_struct_2(_my_struct_2),
an_int(_an_int)
{}
std::string toString() const;
MyStruct2 my_struct_2;
int an_int;
};
The error is:
error C2678: binary '==' : no operator
found which takes a left-hand operand
of type 'myproj::MyStruct1' (or there
is no acceptable conversion)
Why?
In C++, structs do not have a comparison operator generated by default. You need to write your own:
bool operator==(const MyStruct1& lhs, const MyStruct1& rhs)
{
return /* your comparison code goes here */
}
C++20 introduced default comparisons, aka the "spaceship" operator<=>, which allows you to request compiler-generated </<=/==/!=/>=/ and/or > operators with the obvious/naive(?) implementation...
auto operator<=>(const MyClass&) const = default;
...but you can customise that for more complicated situations (discussed below). See here for the language proposal, which contains justifications and discussion. This answer remains relevant for C++17 and earlier, and for insight in to when you should customise the implementation of operator<=>....
It may seem a bit unhelpful of C++ not to have already Standardised this earlier, but often structs/classes have some data members to exclude from comparison (e.g. counters, cached results, container capacity, last operation success/error code, cursors), as well as decisions to make about myriad things including but not limited to:
which fields to compare first, e.g. comparing a particular int member might eliminate 99% of unequal objects very quickly, while a map<string,string> member might often have identical entries and be relatively expensive to compare - if the values are loaded at runtime, the programmer may have insights the compiler can't possibly
in comparing strings: case sensitivity, equivalence of whitespace and separators, escaping conventions...
precision when comparing floats/doubles
whether NaN floating point values should be considered equal
comparing pointers or pointed-to-data (and if the latter, how to know how whether the pointers are to arrays and of how many objects/bytes needing comparison)
whether order matters when comparing unsorted containers (e.g. vector, list), and if so whether it's ok to sort them in-place before comparing vs. using extra memory to sort temporaries each time a comparison is done
how many array elements currently hold valid values that should be compared (is there a size somewhere or a sentinel?)
which member of a union to compare
normalisation: for example, date types may allow out-of-range day-of-month or month-of-year, or a rational/fraction object may have 6/8ths while another has 3/4ers, which for performance reasons they correct lazily with a separate normalisation step; you may have to decide whether to trigger a normalisation before comparison
what to do when weak pointers aren't valid
how to handle members and bases that don't implement operator== themselves (but might have compare() or operator< or str() or getters...)
what locks must be taken while reading/comparing data that other threads may want to update
So, it's kind of nice to have an error until you've explicitly thought about what comparison should mean for your specific structure, rather than letting it compile but not give you a meaningful result at run-time.
All that said, it'd be good if C++ let you say bool operator==() const = default; when you'd decided a "naive" member-by-member == test was ok. Same for !=. Given multiple members/bases, "default" <, <=, >, and >= implementations seem hopeless though - cascading on the basis of order of declaration's possible but very unlikely to be what's wanted, given conflicting imperatives for member ordering (bases being necessarily before members, grouping by accessibility, construction/destruction before dependent use). To be more widely useful, C++ would need a new data member/base annotation system to guide choices - that would be a great thing to have in the Standard though, ideally coupled with AST-based user-defined code generation... I expect it'll happen one day.
Typical implementation of equality operators
A plausible implementation
It's likely that a reasonable and efficient implementation would be:
inline bool operator==(const MyStruct1& lhs, const MyStruct1& rhs)
{
return lhs.my_struct2 == rhs.my_struct2 &&
lhs.an_int == rhs.an_int;
}
Note that this needs an operator== for MyStruct2 too.
Implications of this implementation, and alternatives, are discussed under the heading Discussion of specifics of your MyStruct1 below.
A consistent approach to ==, <, > <= etc
It's easy to leverage std::tuple's comparison operators to compare your own class instances - just use std::tie to create tuples of references to fields in the desired order of comparison. Generalising my example from here:
inline bool operator==(const MyStruct1& lhs, const MyStruct1& rhs)
{
return std::tie(lhs.my_struct2, lhs.an_int) ==
std::tie(rhs.my_struct2, rhs.an_int);
}
inline bool operator<(const MyStruct1& lhs, const MyStruct1& rhs)
{
return std::tie(lhs.my_struct2, lhs.an_int) <
std::tie(rhs.my_struct2, rhs.an_int);
}
// ...etc...
When you "own" (i.e. can edit, a factor with corporate and 3rd party libs) the class you want to compare, and especially with C++14's preparedness to deduce function return type from the return statement, it's often nicer to add a "tie" member function to the class you want to be able to compare:
auto tie() const { return std::tie(my_struct1, an_int); }
Then the comparisons above simplify to:
inline bool operator==(const MyStruct1& lhs, const MyStruct1& rhs)
{
return lhs.tie() == rhs.tie();
}
If you want a fuller set of comparison operators, I suggest boost operators (search for less_than_comparable). If it's unsuitable for some reason, you may or may not like the idea of support macros (online):
#define TIED_OP(STRUCT, OP, GET_FIELDS) \
inline bool operator OP(const STRUCT& lhs, const STRUCT& rhs) \
{ \
return std::tie(GET_FIELDS(lhs)) OP std::tie(GET_FIELDS(rhs)); \
}
#define TIED_COMPARISONS(STRUCT, GET_FIELDS) \
TIED_OP(STRUCT, ==, GET_FIELDS) \
TIED_OP(STRUCT, !=, GET_FIELDS) \
TIED_OP(STRUCT, <, GET_FIELDS) \
TIED_OP(STRUCT, <=, GET_FIELDS) \
TIED_OP(STRUCT, >=, GET_FIELDS) \
TIED_OP(STRUCT, >, GET_FIELDS)
...that can then be used a la...
#define MY_STRUCT_FIELDS(X) X.my_struct2, X.an_int
TIED_COMPARISONS(MyStruct1, MY_STRUCT_FIELDS)
(C++14 member-tie version here)
Discussion of specifics of your MyStruct1
There are implications to the choice to provide a free-standing versus member operator==()...
Freestanding implementation
You have an interesting decision to make. As your class can be implicitly constructed from a MyStruct2, a free-standing / non-member bool operator==(const MyStruct2& lhs, const MyStruct2& rhs) function would support...
my_MyStruct2 == my_MyStruct1
...by first creating a temporary MyStruct1 from my_myStruct2, then doing the comparison. This would definitely leave MyStruct1::an_int set to the constructor's default parameter value of -1. Depending on whether you include an_int comparison in the implementation of your operator==, a MyStruct1 might or might not compare equal to a MyStruct2 that itself compares equal to the MyStruct1's my_struct_2 member! Further, creating a temporary MyStruct1 can be a very inefficient operation, as it involves copying the existing my_struct2 member to a temporary, only to throw it away after the comparison. (Of course, you could prevent this implicit construction of MyStruct1s for comparison by making that constructor explicit or removing the default value for an_int.)
Member implementation
If you want to avoid implicit construction of a MyStruct1 from a MyStruct2, make the comparison operator a member function:
struct MyStruct1
{
...
bool operator==(const MyStruct1& rhs) const
{
return tie() == rhs.tie(); // or another approach as above
}
};
Note the const keyword - only needed for the member implementation - advises the compiler that comparing objects doesn't modify them, so can be allowed on const objects.
Comparing the visible representations
Sometimes the easiest way to get the kind of comparison you want can be...
return lhs.to_string() == rhs.to_string();
...which is often very expensive too - those strings painfully created just to be thrown away! For types with floating point values, comparing visible representations means the number of displayed digits determines the tolerance within which nearly-equal values are treated as equal during comparison.
You need to explicitly define operator == for MyStruct1.
struct MyStruct1 {
bool operator == (const MyStruct1 &rhs) const
{ /* your logic for comparision between "*this" and "rhs" */ }
};
Now the == comparison is legal for 2 such objects.
Starting in C++20, it should be possible to add a full set of default comparison operators (==, <=, etc.) to a class by declaring a default three-way comparison operator ("spaceship" operator), like this:
struct Point {
int x;
int y;
auto operator<=>(const Point&) const = default;
};
With a compliant C++20 compiler, adding that line to MyStruct1 and MyStruct2 may be enough to allow equality comparisons, assuming the definition of MyStruct2 is compatible.
By default structs do not have a == operator. You'll have to write your own implementation:
bool MyStruct1::operator==(const MyStruct1 &other) const {
... // Compare the values, and return a bool result.
}
Comparison doesn't work on structs in C or C++. Compare by fields instead.
Out of the box, the == operator only works for primitives. To get your code to work, you need to overload the == operator for your struct.
Because you did not write a comparison operator for your struct. The compiler does not generate it for you, so if you want comparison, you have to write it yourself.
Related
In the code snippet below, please, if someone can clarify what is the function of bool operator<... and why it is used as a function?
bool operator<(const RankedFace& other) const
{
if (lastDelay == other.lastDelay)
return face.getId() < other.face.getId();
return lastDelay < other.lastDelay;
}
It's the (in class) definition of the operator< for a user defined type (RankedFace I guess).
Thanks to that code, you will be able to compare two objects of type RankedFace with the <, e.g. if( r1 < r2 ) // do something...
It gives the type RankedFace a less-than comparison (operator<). As declared; it looks like a member method. It could also have been a non-member method with the following signature;
bool operator<(const RankedFace& lys, const RankedFace& rhs)
It is typically required for use in the standard library associative containers (std::set etc.).
The associative containers require a comparator to order the objects in them. A custom comparator can be used, but the standard one is std::less which is simply a lhs < rhs.
It allows client code to use the less than comparison on objects of that type (face1 < face2). It is often (not always) implemented together with other comparators (==, !=, <= etc.). If operator< and operator== have been implemented, the remaining ones can be implemented using std::rel_ops.
This is RankedFace's less than operator. It compares two RankedFace objects. For example:
RankedFace foo;
RankedFace bar;
cout << foo < bar ? "foo is smaller than bar" : "bar is greater than or equal to foo";
In the code above foo < bar causes C++ to call foo.operator<(bar).
Dissection of RankedFace::operator< reveals that it:
Considers the object with the lower lastDelay member the smaller object
For objects with identical lastDelays it considers the object which returns a lower getId() the smaller object.
An actual in code comparison between RankedFaces may not exist. The motivation for implementing the less than operator may have been that the less than operator is required to use a RankedFace as in the key in any associative container or unordered associative container.
Is it possible to make a custom operator so you can do things like this?
if ("Hello, world!" contains "Hello") ...
Note: this is a separate question from "Is it a good idea to..." ;)
Yes! (well, sort of)
There are a couple publicly available tools to help you out. Both use preprocessor code generation to create templates which implement the custom operators. These operators consist of one or more built-in operators in conjunction with an identifier.
Since these aren't actually custom operators, but merely tricks of operator overloading, there are a few caveats:
Macros are evil. If you make a mistake, the compiler will be all but entirely useless for tracking down the problem.
Even if you get the macro right, if there is an error in your usage of the operator or in the definition of your operation, the compiler will be only slightly more helpful.
You must use a valid identifier as part of the operator. If you want a more symbol-like operator, you can use _, o or similarly simple alphanumerics.
CustomOperators
While I was working on my own library for this purpose (see below) I came across this project. Here is an example of creating an avg operator:
#define avg BinaryOperatorDefinition(_op_avg, /)
DeclareBinaryOperator(_op_avg)
DeclareOperatorLeftType(_op_avg, /, double);
inline double _op_avg(double l, double r)
{
return (l + r) / 2;
}
BindBinaryOperator(double, _op_avg, /, double, double)
IdOp
What started as an exercise in pure frivolity became my own take on this problem. Here's a similar example:
template<typename T> class AvgOp {
public:
T operator()(const T& left, const T& right)
{
return (left + right) / 2;
}
};
IDOP_CREATE_LEFT_HANDED(<, _avg_, >, AvgOp)
#define avg <_avg_>
Key Differences
CustomOperators supports postfix unary operators
IdOp templates use references rather than pointers to eliminate use of the free store, and to allow full compile-time evaluation of the operation
IdOp allows you to easily specify several operations for the same root identifier
There's a method thoroughly explored in 'Syntactic Aspartame' by Sander Stoks that would allow you to use the following format:
if ("Hello, world!" <contains> "Hello") ...
In essence, you need a proxy object with the operators '<' and '>' overloaded. The proxy does all of the work; 'contains' can just be a singleton with no behavior or data of its own.
// Not my code!
const struct contains_ {} contains;
template <typename T>
struct ContainsProxy
{
ContainsProxy(const T& t): t_(t) {}
const T& t_;
};
template <typename T>
ContainsProxy<T> operator<(const T& lhs, const contains_& rhs)
{
return ContainsProxy<T>(lhs);
}
bool operator>(const ContainsProxy<Rect>& lhs, const Rect& rhs)
{
return lhs.t_.left <= rhs.left &&
lhs.t_.top <= rhs.top &&
lhs.t_.right >= rhs.right &&
lhs.t_.bottom >= rhs.bottom;
}
I've created the following two macros:
#define define const struct
#define operator(ReturnType, OperatorName, FirstOperandType, SecondOperandType) OperatorName ## _ {} OperatorName; template <typename T> struct OperatorName ## Proxy{public:OperatorName ## Proxy(const T& t) : t_(t){}const T& t_;static ReturnType _ ## OperatorName ## _(const FirstOperandType a, const SecondOperandType b);};template <typename T> OperatorName ## Proxy<T> operator<(const T& lhs, const OperatorName ## _& rhs){return OperatorName ## Proxy<T>(lhs);}ReturnType operator>(const OperatorName ## Proxy<FirstOperandType>& lhs, const SecondOperandType& rhs){return OperatorName ## Proxy<FirstOperandType>::_ ## OperatorName ## _(lhs.t_, rhs);}template <typename T> inline ReturnType OperatorName ## Proxy<T>::_ ## OperatorName ## _(const FirstOperandType a, const SecondOperandType b)
Then, you'd have just to define your custom operator as in the following example:
define operator(bool, myOr, bool, bool) { // Arguments are the return type, the name of the operator, the left operand type and the right operand type, respectively
return a || b;
}
#define myOr <myOr> // Finally, you have to define a macro to avoid to put the < and > operator at the start and end of the operator name
Once a time you've set your operator up, you can use it as a predefined operator:
bool a = true myOr false;
// a == true
Warning
While this has been an interesting exercise, it merely demonstrates how bad is to have a macro–enabled precompiler. Adding custom operators like this can easily lead to a sort of metalanguage. Although we know how badly is C++ designed (most of all considering that it was first conceived as a set of extensions for C), we shouldn't be changing it. If you can't use standard C++, which is the only way to keep the code understandable by other people, you should just switch to another language that makes what you wish to do the way you'd like. There are thousands languages — no need to mess around with C++ to make it different.
SHORTLY: You just shouldn't be using this code. You should refrain from using macros unless when only used the same way as inline methods.
To be a bit more accurate, C++ itself only supports creating new overloads of existing operations, NOT creating new operators. There are languages (e.g., ML and most of its descendants) that do allow you to create entirely new operators, but C++ is not one of them.
From the looks of things, (at least) the CustomOperators library mentioned in the other answer doesn't support entirely custom operators either. At least if I'm reading things correctly, it's (internally) translating your custom operator into an overload of an existing operator. That makes things easier, at the expense of some flexibility -- for example, when you create a new operator in ML, you can give it precedence different from that of any built-in operator.
Technically, no. That is to say, you can't extend the set of operator+, operator-, etcetera. But what you're proposing in your example is something else. You are wondering if there is a definition of "contains" such that string-literal "contains" string-literal is an expression, with non-trivial logic (#define contains "" being the trivial case).
There are not many expressions that can have the form string-literal X string-literal. This is because string literals themselves are expressions. So, you're looking for a language rule of the form expr X expr. There are quite a few of those, but they're all rules for operators, and those don't work on strings. Despite the obvious implementation, "Hello, " + "world" is not a valid expression. So, what else can X be in string-literal X string-literal ? It can't be a expression itself. It can't be a typename, a typedef name or a template name. It can't be a function name. It can really only be a macro, which are the only remaining named entities. For that, see the "Yes (well, sort of)" answer.
As others have pointed out you sadly can not write custom operators but with macros you can get similar behaviour. It is actually really easy with c style casting see below.
class To_Range{
public:
size_t start;
size_t end;
To_Range(size_t _start,size_t _end) :
start(_start), end(_end) {}
};
class Slicing_To_End{
public:
int end;
Slicing_To_End(const int& init) : end(init) {}
};
To_Range operator == (const int& start,const Slicing_To_End& end) {
return To_Range(start,end.end);
}
#define to == (Slicing_To_End)
Here 4 to 5 will give back an object of type To_Range. (Slicing_To_End) casts 5 to Slicing_To_End. Now the compiler wants to find an == operator that fits. The only one is our custom operator that takes as input an integer on the first position and in the second Slicing_To_End and returns our type To_Range. You also could return of course other types like int,float.
Your suggestion would be nothing more than syntactic sugar for:
if( contains( "Hello, world!", "Hello" ) ...
and in fact there are already a functions to do that in both cstring and std::string. Which is perhaps a bit like answering "is it a good idea?" but not quite; rather asking "why would you need/want to?"
In my C++ project I frequently encounter inexact results due to numerical errors. Is it somehow possible to somehow redefine the standard comparison operators (==, <=, >=, <, >) so that they do not compare exactly but within an acceptable error (e.g. 1e-12) ?
(If yes, is it a good idea to do this?)
(Of course one could write comparison functions but people intuitively use the operators.)
To overload operators some argument must be user-defined type. The built-in ones are fixed and unchangeable.
But even if you could it would hardly be a good thing. Do yourself a favor, and provide your custom compare "operators" as a set of functions, choosing a name that implies the strategy they use. You can't expect a code reader to know without proper indication that equal means strict or with DBL_EPSILON or 2*DBL_EPSILON or some arbitrary linear or scaled tolerance.
You can't overload the operators for standard types (int, float, char, etc)
You could of course declare a type:
class Float
{
private:
float f;
public:
Float(float v) : f(v) {}
... bunch of other constructors.
friend bool operator==(Float &a, Float &b);
... more operators here.
float operator float() { return f; }
};
bool operator==(Float &a, Float &b) { return (fabs(b.f-a.f) < epsilon); }
bool operator==(Float &a, const float &b) { return (fabs(b-a.f) < epsilon); }
... several other operator declarations - need on also make operator
(The above code is "as an idea", not tested and perhaps need more work to be "good").
You would of course then need some ugly typedef or macro to replace "float" with "Float" everywhere in the code.
No, you cannot overload operators for built-in types.
No, changing the semantics of operators is (in general) not a good idea.
You could either:
Use comparison-functions (as you suggest yourself).
Write a wrapper-class around a double member that has the operators you want.
I am playing around with the new explicit for cast-operators. If you write something like
struct Data {
explicit operator string();
};
It is not possible to accidentally convert Data to string. The darget data type bool is an exception: In certain cases the implicit conversion is allowed even if it is marked explicit -- contextual conversion. So, you can use this data types in an if(...) for example:
struct Ok {
explicit operator bool(); // allowed in if(...) anyway
};
The paragraph "25.4.(2) Sorting and related operations" seems to allow this for the Compare functor of standard containers like set as well. But my tries with gcc-4.7.0 fail, and I am note sure if it is my mis-understanding or a bug in gcc?
#include <set>
struct YesNo { // Return value type of Comperator
int val_;
explicit YesNo(int y) : val_{y} {}
/* explicit */ operator bool() { return val_!=0; }
};
static const YesNo yes{1};
static const YesNo no{0};
struct LessYesNo { // Comperator with special return values
YesNo operator()(int a, int b) const {
return a<b ? yes : no;
}
};
int main() {
std::set<int,LessYesNo> data {2,3,4,1,2};
}
Without the explicit before operator bool() the example compiles. And my understanding of "25.4.(2)" is, that this should also compile with the `explicit.
Did I understand the Std correctly that for set also explicit bool conversions should work? And might this be a bug in gcc then, or did I understand something wrong?
My reading of the standard is a little different -
section 25.4 deals with sorting algorithms rather than for sorted containers; the context established in 25.4.(1) means that the property of the compare object specified in 25.4.(2) applies to the algorithms in 25.4, not to sorted containers
1
All the operations in 25.4 have two versions: one that takes a
function object of type Compare and one that uses an operator.
2
Compare is a function object type (20.8). The return value of the
function call operation applied to an object of type Compare, when
contextually converted to bool (4), yields true if the first argument
of the call is less than the second, and false otherwise. Compare comp
is used throughout for algorithms assuming an ordering relation. It is
assumed that comp will not apply any non-constant function through the
dereferenced iterator.
I don't know whether your example should work or not, but I don't think section 25.4 is applicable here.
A quick test with a vector and std::sort works:
#include <list>
#include <algorithm>
struct YesNo { // Return value type of Comperator
int val_;
explicit YesNo(int y) : val_{y} {}
explicit operator bool() { return val_!=0; }
};
static const YesNo yes{1};
static const YesNo no{0};
struct LessYesNo { // Comperator with special return values
YesNo operator()(int a, int b) const {
return a<b ? yes : no;
}
};
int main() {
std::vector<int> data {2,3,4,1,2};
std::sort(std::begin(data), std::end(data), LessYesNo());
}
Edit:
The associative container's Compare parameter is defined in terms of secion 25.4:
1 Associative containers provide fast retrieval of data based on keys. The library provides four basic kinds of associative containers: set, multiset, map and multimap.
2 Each associative container is parameterized on Key and an ordering relation Compare that
induces a strict weak ordering (25.4) on elements of Key. In addition, map and multimap associate an arbitrary type T with the Key. The object of type Compare is called the comparison object of a container.
and 23. has no other conditions on the type of Compare as far as I can see, so it does seem reasonable to assume that a type satisfying the constraints of 25.4 are equally applicable.
Did I understand the Std correctly that for set also explicit bool conversions should work?
This is sort of a grey area of the specification. The return value from the comparison function is required to be "convertible to bool". But what that means in light of explicit operator bool() is unclear.
For example, one could write std::set's comparison usage as this:
CompFunc functor;
if(functor(input, currVal))
...
Or, one could do this:
CompFunc functor;
bool test = functor(input, currVal);
if(test)
...
Are both of these technically legal under C++11? No idea. Obviously the second one fails if operator bool() is explicit.
I looked at the definition of std::shared_ptr, and it has an explicit operator bool() as well. It also says that std::shared_ptr is "convertible to bool", in section 20.7.2.2, paragraph 2.
So I'm guessing that the second version should be implemented as follows:
CompFunc functor;
bool test = static_cast<bool>(functor(input, currVal));
if(test)
...
The fact that it's not explicitly stated anywhere in the spec means that it should be filed as a defect report. But it should probably also be filed as a GCC/libstdc++ bug.
Personally, to be safe, I wouldn't rely on it.
On Contextual Conversion
Section 4, paragraph 3 states:
An expression e appearing in such a context is said to be contextually converted to bool and is well-formed if and only if the declaration bool t(e); is well-formed, for some invented temporary variable t
So operations which are "contextually convertible to bool" means that explicit operator bool() will work. Since std::set's "Compare" functor must fall under the requirements of 25.4, and these requirements include "contextually converted to bool", it looks like a GCC/libstdc++ bug.
I'd still avoid doing it when you can help it, though.
Sometimes I have structs such as this --
struct aggregate1 {
std::string name;
std::vector<ValueT> options;
size_t foobar;
// ...
};
-- where (in)equality is simply defined as (in)equality of all members: lhs_name == rhs_name && lhs_options == rhs_options && lhs_foobar == rhs_foobar.
What's the "best" way to implement this? (Best as in: (Runtime-)Efficiency, Maintainability, Readability)
operator== in terms of operator!=
operator!= in terms of operator==
Separate implementations for == and !=
As member or as free functions?
Note that this question is only about the (in)equality ops, as comparison (<, <=, ...) doesn't make too much sense for such aggregates.
I would do this but maybe move operator== definition to cpp file. Leave operator!= to be inline
Remember to compare member variables that are most likely to differ first so the rest are short-circuited and performance is better.
struct aggregate1 {
bool operator==(const aggregate1& rhs) const
{
return (name == rhs.name)
&& (options == rhs.options)
&& (foobar == rhs.foobar);
}
bool operator!=(const aggregate1& rhs) const
{
return !operator==(rhs);
}
std::string name;
std::vector<ValueT> options;
size_t foobar;
// ...
};
Member or free function is a matter of taste, and writing separate implementations of == and != seems to me boring, error-prone (you may forget a member in just one of the two operators, and it will take time to notice) without adding anything in terms of efficiency (calling the other operator and applying ! has a negligible cost).
The decision is restricted to "is it better to implement operator== in terms of operator!= or the contrary?
In my opinion, in terms of maintainability/readability/efficiency it's the same; I'd only recommend to do it in the same way everywhere for the sake of consistency. The only case where you'd want to prefer to use one or the other as the "base operator" is when you know that, in the types contained in your structure, that operator is faster than its negation, but I don't know when this could happen.
In C++20, implementing equality and inequality operators can be as simple as declaring operator== as default:
struct S {
int x;
// ...
// As member function
bool operator==(S const &) const = default;
// As non-member function (hidden friend)
// friend bool operator==(S const &, S const &) = default;
};
If only operator== is provided, a!=b is interpreted as !(a==b) according to overload resolution, so there is no need for providing an explicit overload for operator!=.
I would argue that defaulting operator== as a hidden friend is preferable because it works with reference-wrapped objects:
S s;
auto rs{std::ref(s)};
rs==rs; // OK for hidden friend; ill-formed if declared as member function
In this example, operator== is not defined for std::reference_wrapper<S>, but argument-dependent lookup (ADL) can select the hidden friend with operands implicitly-converted to S const &. Notice, however, that ::operator==(rs,rs) will only work if operator== is defined as a free function because ADL is not triggered for qualified names.
IMHO, implement as friends and implement the operator== (some STL algorithms will rely on this for example) and the operator!= should be implemented as the negation of the equals operator.
(-: Self answer :-)
I would like to highlight one aspect of aggregates WRT efficiency:
The order of evaluation of op== and op!= is irrelevant for (average) performance.
Assuming separate implementations for now and given the two extremes (a-eq) all subelements equal and (b-neq) all subelements inequal, we have these cases:
(a-eq) + operator== : Needs to compare all sub elements to return true
(a-eq) + operator!= : Needs to compare all sub elements to return false
(b-neq) + operator== : Returns false after 1st sub element is determined inequal
(b-neq) + operator!= : Returns true after 1st sub element is determined inequal
Since performance on average is the same either way it seems -- at least to me -- more natural to implement op!= in terms of op==, as it feels more natural to me to implement the equality op.