The question of why you have to provide operator==
is not the same as the question of why you have to provide some comparison function.
Regarding the latter, the reason that you are required to provide the comparison logic, is that element-wise equality is seldom appropriate. Consider, for example, a POD struct with an array of char
in there. If it’s being used to hold a zero-terminated string, then two such structs can compare unequal at the binary level (due to arbitrary contents after the zero bytes in the strings) yet being logically equivalent.
In addition, there are all the C++ level complications mentioned by other answers here, e.g. the especially thorny one of polymorphic equality (you really don’t want the compiler to choose!).
So, essentially, there is simply no good default choice, so the choice is yours.
Regarding the former question, which is what you literally asked, why do you have to provide operator==
?
If you define operator<
and operator==
, then the operator definitions in namespace std::rel_ops
can fill in the rest for you. Presumably the reason why operator==
is needed is that it would be needlessly inefficient to implement it in terms of operator<
(then requiring two comparisons). However, the choice of these two operators as basis is thoroughly baffling, because it makes user code verbose and complicated, and in some cases much less efficient than possible!
The IMHO best basis for comparison operators is instead the three-valued compare
function, such as std::string::compare
.
Given a member function variant comparedTo
, you can then use a Curiously Recurring Template Pattern class like the one below, to provide the full set of operators:
template< class Derived >
class ComparisionOps
{
public:
friend int compare( Derived const a, Derived const& b )
{
return a.comparedTo( b );
}
friend bool operator<( Derived const a, Derived const b )
{
return (compare( a, b ) < 0);
}
friend bool operator<=( Derived const a, Derived const b )
{
return (compare( a, b ) <= 0);
}
friend bool operator==( Derived const a, Derived const b )
{
return (compare( a, b ) == 0);
}
friend bool operator>=( Derived const a, Derived const b )
{
return (compare( a, b ) >= 0);
}
friend bool operator>( Derived const a, Derived const b )
{
return (compare( a, b ) > 0);
}
friend bool operator!=( Derived const a, Derived const b )
{
return (compare( a, b ) != 0);
}
};
where compare
is an overloaded function, e.g. like this:
template< class Type >
inline bool lt( Type const& a, Type const& b )
{
return std::less<Type>()( a, b );
}
template< class Type >
inline bool eq( Type const& a, Type const& b )
{
return std::equal_to<Type>()( a, b );
}
template< class Type >
inline int compare( Type const& a, Type const b )
{
return (lt( a, b )? -1 : eq( a, b )? 0 : +1);
}
template< class Char >
inline int compare( basic_string<Char> const& a, basic_string<Char> const& b )
{
return a.compare( b );
}
template< class Char >
inline int compareCStrings( Char const a[], Char const b[] )
{
typedef char_traits<Char> Traits;
Size const aLen = Traits::length( a );
Size const bLen = Traits::length( b );
// Since there can be negative Char values, cannot rely on comparision stopping
// at zero termination (this can probably be much optimized at assembly level):
int const way = Traits::compare( a, b, min( aLen, bLen ) );
return (way == 0? compare( aLen, bLen ) : way);
}
inline int compare( char const a[], char const b[] )
{
return compareCStrings( a, b );
}
inline int compare( wchar_t const a[], wchar_t const b[] )
{
return compareCStrings( a, b );
}
Now, that’s the machinery. What does it look like to apply it to your class …
struct Vec3
{
float x, y, z;
};
?
Well it’s pretty simple:
struct Vec3
: public ComparisionOps<Vec3>
{
float x, y, z;
int comparedTo( Vec3 const& other ) const
{
if( int c = compare( x, other.x ) ) { return c; }
if( int c = compare( y, other.y ) ) { return c; }
if( int c = compare( z, other.z ) ) { return c; }
return 0; // Equal.
}
};
Disclaimer: not very tested code… :-)
memcmp
like(memcmp(&veca, &vecb, sizeof(Vec3)) == 0)
to check ifveca == vecb
– Louannlouannastruct
is that it's not nice to guess whether some tolerance for small rounding errors should be used, whether + and - zero should be deemed equal, how NaN should be handled... the same issues apply for individual builtin floats and doubles, but for structs it's not bad to had a compiler encourage you to think about what's appropriate rather than compile code that may be broken for your needs. – Lenhart