Why do I have to overload operator== in POD types?

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.


C++20 adds this capability:

struct Vec3 {
    float x, y, z;
    auto operator<=>(const Vec3&) const = default;
    bool operator==(X const&) const = default;
}

This is currently only implemented in GCC and clang trunk. Note that currently defaulting operator<=> is equivalent to also defaulting operator==, however there is an accepted proposal to remove this. The proposal suggests having defaulting operator<=> also imply (not be equivalent to as it is today) defaulting operator== as an extension.

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


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