The autogenerated Equality Operator

You can define the three-way comparison operator or request it from the compiler with =default. But do you know that you can also define or request the equality operator in C++20?

Before I dive into the autogenerated equality operator, I’d like to refresh your knowledge about the three-way comparison operator.

The Three-Way Comparison Operator

You can define the three-way comparison operator or request it from the compiler with =default. You get all six comparison operators in both cases: ==, !=, <, <=, >, and >=.

// threeWayComparison.cpp

#include <compare>
#include <iostream>

struct MyInt {
    int value;
    explicit MyInt(int val): value{val} { }
    auto operator<=>(const MyInt& rhs) const {           // (1)      
        return value <=> rhs.value;
    }
};

struct MyDouble {
    double value;
    explicit constexpr MyDouble(double val): value{val} { }
    auto operator<=>(const MyDouble&) const = default;   // (2)
};

template <typename T>
constexpr bool isLessThan(const T& lhs, const T& rhs) {
    return lhs < rhs;
}

int main() {
    
    std::cout << std::boolalpha << std::endl;
    
    MyInt myInt1(2011);
    MyInt myInt2(2014);
    
    std::cout << "isLessThan(myInt1, myInt2): "
              << isLessThan(myInt1, myInt2) << std::endl;
              
    MyDouble myDouble1(2011);
    MyDouble myDouble2(2014);
    
    std::cout << "isLessThan(myDouble1, myDouble2): "
              << isLessThan(myDouble1, myDouble2) << std::endl;          
              
    std::cout << std::endl;
              
}

The user-defined (1) and the compiler-generated (2) three-way comparison operators work as expected.

But there are a few subtle differences in this case. The compiler-deduced return type for MyInt (1) supports strong ordering, and the compiler-deduced return type of MyDouble supports partial ordering. Floating pointer numbers only support partial ordering because floating-point values such as NaN (Not a Number) can not be ordered. For example, NaN == NaN  is false.

The compiler-generated three-way comparison operator needs the header <compare>, which is implicit constexpr and noexcept. Additionally, it performs a lexicographical comparison. Lexicographical comparison means that all base classes are compared left to right and all non-static members in their declaration order.

Let’s assume I add a std::unordered_set to both classes MyInt and MyDouble.

struct MyInt {
    int value;
    std::unordered_set<int> mySet;
    explicit MyInt(int val): value{val}, mySet{val} { }
    bool operator<=>(const MyInt& rhs) const {
        if (auto first = value <=> rhs.value; first != 0) return first;
        else return mySet <=> rhs.mySet; 
    }
};

struct MyDouble {
    double value;
    std::unordered_set<double> mySet;
    explicit MyDouble(double val): value{val}, mySet{val} { }
    bool operator<=>(const MyDouble&) const = default;   
};

Requesting or defining the three-way comparison fails because std::unordered_set does not support ordering. std::unordered_set only support equality comparison, and so does MyInt and MyDouble.

Equality Operator

When you define or request the equality operator from the compiler with =default, you automatically
get the equality and inequality operators: ==, and !=.

// equalityOperator.cpp

#include <iostream>
#include <tuple>
#include <unordered_set>

struct MyInt {
    int value;
    std::unordered_set<int> mySet;
    explicit MyInt(int val): value{val}, mySet{val} { }
    bool operator==(const MyInt& rhs) const {                 
        return std::tie(value, mySet) == std::tie(rhs.value, rhs.mySet);
    }
};

struct MyDouble {
    double value;
    std::unordered_set<double> mySet;
    explicit MyDouble(double val): value{val}, mySet{val} { }
    bool operator==(const MyDouble&) const = default;   
};

template <typename T>
constexpr bool areEqual(const T& lhs, const T& rhs) {

    return lhs == rhs;
}

template <typename T>
constexpr bool areNotEqual(const T& lhs, const T& rhs) {

    return lhs != rhs;
}

int main() {
    
    std::cout << std::boolalpha << '\n';
    
    MyInt myInt1(2011);
    MyInt myInt2(2014);
    
    std::cout << "areEqual(myInt1, myInt2): "
              << areEqual(myInt1, myInt2) << '\n';
    std::cout << "areNotEqual(myInt1, myInt2): "
              << areNotEqual(myInt1, myInt2) << '\n';

    std::cout << '\n';          
              
    MyDouble myDouble1(2011.0);
    MyDouble myDouble2(2014.0);
    
    std::cout << "areEqual(myDouble1, myDouble2): "
              << areEqual(myDouble1, myDouble2) << '\n';
    std::cout << "areNotEqual(myDouble1, myDouble2): "
              << areNotEqual(myDouble1, myDouble2) << '\n';           
              
    std::cout << '\n';
              
}

Now, I can compare MyInt and MyDouble for equality and inequality.

Rainer D 6 P2 500x500

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    I applied a neat trick in the program equalityOperator.cpp. Can you spot it?

    In the following example, I implemented the equality operator of MyInt by chaining the equality operators of value and mySet.

    struct MyInt {
        int value;
        std::unordered_set<int> mySet;
        explicit MyInt(int val): value{val}, mySet{val} { }
        bool operator==(const MyInt& rhs) const {
            if (auto first = value == rhs.value; first != 0) return first;
            else return mySet == rhs.mySet; 
        }
    };
    

    This is pretty ugly and error-prone if you have a class with more members

    On the contrary, I used std::tie to implement the equality operator in the program equalityOperator.cpp.

    struct MyInt {
        int value;
        std::unordered_set<int> mySet;
        explicit MyInt(int val): value{val}, mySet{val} { }
        bool operator==(const MyInt& rhs) const {                 
            return std::tie(value, mySet) == std::tie(rhs.value, rhs.mySet);
        }
    };
    

    std::tie creates a tuple of lvalue references to its arguments. Finally, the created tuples are lexicographically compared.

    What’s Next?

    In my next post, I will continue my journey through C++20 and write about std::span. std::span represents an object that refers to a contiguous sequence of objects.

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,