c++c++20spaceship-operator

Why are explicitly defaulted comparison operators deleted when the class contains a reference member?


I tried to default both operator== and operator<=> in a simple class that contains a reference member like that:

#include <iostream>
#include <string>

class Simple
{
public:
    Simple(const std::string& data)
     : data_(data)
    {
    }

    auto operator<=>(const Simple&) const = default;

private:
    const std::string& data_;
};

int main()
{
    std::string str1 = "one";
    Simple s1(str1);
    std::string str2 = "two";
    Simple s2(str2);
    std::cout << (s1 < s2) << std::endl; // compiler error

    return 0;
}

clang compiler states that

warning: explicitly defaulted three-way comparison operator is implicitly deleted
note: defaulted 'operator<=>' is implicitly deleted because class 'Simple' has a reference member

I got no warnings from other compilers (e.g. MSVC), but when I try to use it, I get compile errors:

<source>(62): error C2280: 'auto Simple::operator <=>(const Simple &) const': attempting to reference a deleted function
<source>(49): note: see declaration of 'Simple::operator <=>'
<source>(49): note: 'auto Simple::operator <=>(const Simple &) const': function was implicitly deleted because 'Simple' data member 'Simple::data_' of type 'const std::string &' is a reference type
<source>(52): note: see declaration of 'Simple::data_'

Other defaulted functions like copy assignment will of course be deleted, because they are not possible with a reference member.

But why can't the content a reference points to not be automatically compared?

And what is the shortest way to implement it manually?


Solution

  • The issue here is that operator<=> and operator== cannot be defaulted in classes with reference members because the default implementations rely on comparing all data members directly. However, references don't support copy assignment or move operations, so the compiler can't synthesize these operators when references are involved.

    When the compiler tries to default the operator<=>, it attempts to compare each member individually. But since data_ is a reference, it doesn't have a value of its own — it only points to another object, so the compiler can't perform direct comparisons like it would for a value type. That’s why the comparison operator is implicitly deleted.

    You’ll need to implement operator== and operator<=> manually to compare the object the reference points to rather than the reference itself. Here’s how you can do it:

    #include <string>
    #include <iostream>
    #include <compare>
    
    class Simple
    {
    public:
        Simple(const std::string& data)
            : data_(data)
        {
        }
    
        bool operator==(const Simple& other) const
        {
            return data_ == other.data_;
        }
    
        auto operator<=>(const Simple& other) const
        {
            return data_ <=> other.data_;
        }
    
    private:
        const std::string& data_;
    };
    
    int main()
    {
        std::string str1 = "one";
        Simple s1(str1);
        std::string str2 = "two";
        Simple s2(str2);
        
        std::cout << (s1 < s2) << std::endl;  // Works now
    
        return 0;
    }
    
    1. operator==: We manually define operator== to compare data_ in s1 and s2 by comparing the objects they reference.

    2. operator<=>: Similarly, we manually define operator<=> to compare data_ in s1 and s2 using operator<=> on std::string. Since data_ is a reference, data_ <=> other.data_ performs a comparison between the std::string objects that the references point to.

    Since references are not values themselves, they need to be compared by the objects they reference. This manual implementation works around the compiler's inability to default operator== and operator<=> for types with reference members. This is the shortest and cleanest way to handle the situation without changing the design of the class.