scales 310962 640

C++ Core Guidelines: Comparison, Swap, and Hash

This post will be about comparisons, swap, and hash. That means I conclude with his post my treatise about default operations rules in C++.

scales 310962 640 

Here are the nine rules.

 

Let’s dive into the details.

Default operation rules:

C.80: Use =default if you have to be explicit about using the default semantics

Do you remember the rule of five? It means if you define one of the five special member functions, you must define all.

Here is the point.

When I implement the destructor, such as in the following example, I have to define the copy and move constructor and assignment operator.

 

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.

     

    class Tracer {
        string message;
    public:
        Tracer(const string& m) : message{m} { cerr << "entering " << message << '\n'; }
        ~Tracer() { cerr << "exiting " << message << '\n'; }
    
        Tracer(const Tracer&) = default;
        Tracer& operator=(const Tracer&) = default;
        Tracer(Tracer&&) = default;
        Tracer& operator=(Tracer&&) = default;
    };
    

     

    That was easy! Right? But I can also do it alone, which is at least uninteresting and error-prone. 

     

    class Tracer2 {
        string message;
    public:
        Tracer2(const string& m) : message{m} { cerr << "entering " << message << '\n'; }
        ~Tracer2() { cerr << "exiting " << message << '\n'; }
    
        Tracer2(const Tracer2& a) : message{a.message} {}
        Tracer2& operator=(const Tracer2& a) { message = a.message; return *this; }
        Tracer2(Tracer2&& a) :message{a.message} {}
        Tracer2& operator=(Tracer2&& a) { message = a.message; return *this; }
    };
    

     

    C.81: Use =delete when you want to disable default behavior (without wanting an alternative)

    Sometimes, you want to disable the default operations. Here comes delete into the play. C++ eats its own dog food. The copy constructor of types such as locks, mutexes, promises, or futures is set to delete. The same holds true for the smart pointer std::unique_ptr: std::unique_ptr(const std::unique_ptr&) = delete.

    You can use delete to create strange types. Instances of Immortal cannot be destroyed.

    class Immortal {
    public:
        ~Immortal() = delete;   // do not allow destruction
        // ...
    };
    
    void use()
    {
        Immortal ugh;   // error: ugh cannot be destroyed
        Immortal* p = new Immortal{};
        delete p;       // error: cannot destroy *p
    }
    

    C.82: Don’t call virtual functions in constructors and destructors

    This rule is quite similar to rule C.50: Use a factory function if you need “virtual behavior” during initialization which I presented in the post C++ Core Guidelines: Constructors.

    The next three rules are about swap functions. Let’s do it together.

    C.83: For value-like types, consider providing a noexcept swap function, C.84: A swap may not fail, and C.85: Make swap noexcept

    A swap function is quite handy.

    template< typename T >
    void std::swap(T & a, T & b) noexcept {
        T tmp(std::move(a));
        a = std::move(b);
        b = std::move(tmp);
    }
    

     

    The C++ standard offers more than 40 specializations for std::swap. You can use it as a building block for many idioms, such as copy construction/assignment. A swap function should not fail; therefore, you must declare it as noexcept.

    Here is an example of a move assignment operation using std::swap.  pdata points to an array.

    class Cont{     
    public:
      Cont& operator=(Cont&& rhs);
     
    private:
      int *pData;           
    };
    
    Cont& Cont::operator=(Cont&& rhs){
      std::swap(pData, rhs.pData);
      return *this;
    }
    

     

    C.86: Make == symmetric with respect of operand types and noexcept

    If you don’t want to surprise your user, you should make the == operator symmetric.

    Here is an unintuitive == operator, which is defined inside the class.

    class MyNumber {
        int num;
    public:
        MyNumber(int n): num(n){};
        bool operator==(const MyNumber& rhs) const { return num == rhs.num; }
    };
    
    int main(){
     MyNumber(5) == 5;
        // 5 == MyNumber(5);
    }
    

     

    The call MyNumber(5) == 5 is valid because the constructor converts the int argument to an instance of MyNumber. The last line gives an error.  The comparison operator for natural numbers will not accept an instance of MyNumber.

    The elegant way to solve this asymmetry is to declare a friend operator== inside the class MyNumber. Here is the second version of MyNumber.

    class MyNumber {
        int num;
    public:
        MyNumber(int n): num(n){};
        bool operator==(const MyNumber& rhs) const { return num == rhs.num; }
        friend bool operator==(const int& lhs, const MyNumber& rhs){ 
            return lhs == rhs.num; 
        }
    };
    
    int main(){
        MyNumber(5) == 5;
        5 == MyNumber(5);
    }
    

     

     The surprises continue.

    C.87: Beware of == on base classes

    Writing a foolproof == operator for a hierarchy is hard. The guidelines give an excellent example of such a challenging job. Here is the hierarchy.

    class B {
        string name;
        int number;
        virtual bool operator==(const B& a) const
        {
             return name == a.name && number == a.number;
        }
        // ...
    };
    
    class D :B {
        char character;
        virtual bool operator==(const D& a) const
        {
            return name == a.name && number == a.number && character == a.character;
        }
        // ...
    };
    

     

    Let’s try it out.

    B b = ...
    D d = ...
    b == d;      // compares name and number, ignores d's character            // (1)
    d == b;      // error: no == defined                                       // (2) 
    D d2;
    d == d2;
    // compares, name, number, and character
    B& b2 = d2;
    b2 == d; // compares name and number, ignores d2's and d's character // (1)

     

    Comparing instances of B or instances of D will work. But mixing instances of B and D will not work as expected. Using B’s == operator ignores D‘s character (1). Using D‘s operator will not work for instances of B (3). The last line is quite tricky. The == operator of B is used. Why? The == operator of D overwrote the == operator of B.Really? No! Both operators have different signatures. One taking an instance of B; the other taking an instance of D. D‘s version will not overwrite B‘s version.

    This observation will also hold for the other five comparison operators: !=, <, <=, >, and >=.

    C.89: Make a hash noexcept

    Hash functions are implicitly used by unordered associative containers such as std::unordered_map. The user doesn’t expect that they will throw. If you want to use your own type as a key in an unordered associative container, you must define a hash function for the key.

    Do it by using the std::hash function for the attributes of your class and combining them with ^ (xor).

     

    struct MyKey{
      int valInt = 5;
      double valDou = 5.5;
    };
    
    struct MyHash{
      std::size_t operator()(MyKey m) const {
        std::hash<int> hashVal1;
        std::hash<double> hashVal2;
        return hashVal1(m.valInt) ^ hashVal2(m.valDou);
      }
    };
    

     

    What’s next?

    Following the guidelines, the next topic should be containers and other resource handles, but only the names of the rules are available. Therefore I will skip this part and go straight to lambda expressions in the next post.

     

     

     

    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,and Matt Godbolt.

    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,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *