Why do I have to overload operator== instead of it being defined implicitly? [duplicate]
Asked Answered
F

5

22

I have a struct that's defined like this:

struct Vec3 {
    float x, y, z;
};

When I attempted to use std::unique on a std::vector<Vec3>, I was met with this error:

Description Resource Path Location Type no match for ‘operator==’ in ‘__first.__gnu_cxx::__normal_iterator<_Iterator, _Container>::operator* with _Iterator = Vec3*, _Container = std::vector<Vec3, std::allocator > == __next.__gnu_cxx::__normal_iterator<_Iterator, _Container>::operator* with _Iterator = Vec3*, _Container = std::vector<Vec3, std::allocator >’ ModelConverter line 4351, external location: /usr/include/c++/4.4.6/bits/stl_algo.h C/C++ Problem

I understand the the necessity of the naievite of the compiler in inequality operators and others (in this case, * would almost certainly not be what I mean), but is this a matter of policy, or is there a technical reason for it that I'm not aware of? There's a default assignment operator, so why no default equality operator?

Fabyola answered 25/3, 2012 at 1:11 Comment(5)
Because there's no such thing as an appropriate "default behavior" when it comes to equality.Mobilize
@EtiennedeMartel: There could be. In Ada, for example, equality on records is defined in terms of equality of the components.Katherinakatherine
Equality and inequality are equally easy to define.Katherinakatherine
you can use memory comparision memcmp like (memcmp(&veca, &vecb, sizeof(Vec3)) == 0) to check if veca == vecbLouannlouanna
I address this in this answer - main relevant bit is for your specific struct 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
D
19

There's no technical reason. Pedantically, you might say this is because C doesn't let you compare two structures with ==, and this is a good reason; that behavior switching when you go to C++ is non-obvious. (Presumably, the reason that C doesn't support that is that field-wise comparison might work for some structs, but definitely not all.)

And just from a C++ point of view, what if you have a private field? A default == technically exposes that field (indirectly, but still). So would the compiler only generate an operator== if there are no private or protected data members?

Also, there are classes that have no reasonable definition of equality (empty classes, classes that do not model state but cache it, etc.), or for whom the default equality check might be extremely confusing (classes that wrap pointers).

And then there's inheritance. Deciding what to do for operator== in a situation of inheritance is complicated, and it'd be easy for the compiler to make the wrong decision. (For example, if this was what C++ did, we would probably be getting questions about why == always succeed when you test equality between two objects that are both descendants of an abstract base class and being used with a reference to it.)

Basically, it's a thorny problem, and it's safer for the compiler to stay out of it, even considering that you could override whatever the compiler decided.

Dicarlo answered 25/3, 2012 at 1:18 Comment(1)
Thanks, I wasn't aware of that aspect of C, and that there could be classes that appear as simple aggregate containers, but don't have a concept of equality is a good one.Fabyola
S
5

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… :-)

Swordbill answered 25/3, 2012 at 1:49 Comment(1)
That was a great answer. The CRTP is something that I wish I knew more about, thank you for showing it in context! I'd also never heard of std::rel_ops, so I'll have to look in to that.Fabyola
R
5

C++20 added this capability in P0515R3, with further changes made by proposals such as P1185R1.

struct Vec3 {
    float x, y, z;

    // provides <=>, <, >, <=, >=, ==, and !=:
    auto operator<=>(const Vec3&) const = default;
    
    // unnecessary in this case, but valid if we only want to default == and !=:
    bool operator==(X const&) const = default;
};

Note that defaulting operator<=> also defaults operator== (although == isn't implemented in terms of <=>).

Microsoft has documentation on this feature at https://devblogs.microsoft.com/cppblog/simplify-your-code-with-rocket-science-c20s-spaceship-operator/.

Ranna answered 14/1, 2020 at 18:9 Comment(0)
D
1

Another reason to write your own comparison operator that I don't see in the other answers is potential performance improvements. Maybe your class has three main members, with the others used for caching generated results or some such. The classes may be deemed equal if the main members are equal, and there would be no need to check the rest, and you would not want the compiler to do so if the checks might be expensive, are performed frequently, or give completely wrong results.

For example, a position is equal if the first three components are equal, but how the position has been localized for display does not matter (units of measure, language, precision, etc.), in case there is some reason to cache the result:

struct Posn {
    double      x;
    double      y;
    double      z;
    std::string cachedLocalizedDisplay;
}
Distend answered 16/5, 2024 at 7:34 Comment(0)
B
0

What would you like the equality operation to be? All the fields the same? It's not gonna make that decision for you.

Birchfield answered 25/3, 2012 at 1:13 Comment(5)
In this circumstance, the operator== I defined did just that. I can see where you might want additional complexity in your comparitor, but I can't think of a circumstance in which two structs with the exact same data would be unequal. Can you think of an example?Fabyola
It's the same question as 'what would be the default copy constructor or assignment operator?"Narrow
K.G.: an example would be where two objects are equal if they have the same identity (=address)Narrow
@K.G. - I'm not saying I don't think there should be a default defined, but that's just the way it was defined. And Karoly's example is realisticBirchfield
@KarolyHorvath Thanks, that's something I hadn't thought of.Fabyola

© 2022 - 2025 — McMap. All rights reserved.