Pet programming peeves . . .

Simplifying relational operators in C++
© Conrad Weisert, Information Disciplines, Inc., Chicago
3 February 2010

NOTE: This document may be circulated or quoted from freely, as long as the copyright credit is included.


What's wrong with this C++ class definition fragment?


 //  Weight class
 //  ------------

 #ifndef WEIGHT
  define WEIGHT const Weight

  class Weight {
     long  pounds;
     short ounces;
  public:
     .
     .
 //  Relational operators 
 //  --------------------	  
   
  friend bool operator==(WEIGHT ls, WEIGHT rs)
   {return ls.pounds==rs.pounds && ls.ounces==rs.ounces;}
  friend bool operator!=(WEIGHT ls, WEIGHT rs)
   {return ls.pounds!=rs.pounds || ls.ounces!=rs.ounces;}
  friend bool operator<(WEIGHT ls, WEIGHT rs)
   {return ls.pounds < rs.pounds 
     ||   (ls.pounds== rs.pounds && ls.ounces < rs.ounces ;}
  friend bool operator<=(WEIGHT ls, WEIGHT rs)
   {return ls.pounds < rs.pounds 
     ||   (ls.pounds== rs.pounds && ls.ounces <= rs.ounces ;}
  friend bool operator>(WEIGHT ls, WEIGHT rs)
   {return ls.pounds > rs.pounds 
     ||   (ls.pounds== rs.pounds && ls.ounces > rs.ounces ;}
  friend bool operator>=(WEIGHT ls, WEIGHT rs)
   {return ls.pounds > rs.pounds 
     ||   (ls.pounds== rs.pounds && ls.ounces >= rs.ounces ;}
     .
     .
  };
 #endif

Quite a lot!

The six relational operator definitions contain a lot of detail, much of it repetitive. If a student turned in such a class definition as part of an assignment, I would pose to him or her these two questions:

  1. How many test cases would you need to feel certain that all six overloaded operator functions are correct?
  2. What would you do if you learned that on your platform the following expression is a lot more efficient than the one in the above less-than operator:
     (16*ls.pounds + ls.ounces) < (16*rs.pounds + rs.ounces)
Both questions should make the programmer uncomfortable.

Minimizing repetition by factoring out commonality

Readers of these pages know that we consider unnecessary repetition one of the major faults in software quality. Those relational operator functions are so similar to one another that there must be a way to eliminate some repetition. An obvious place to start is the non-equals operator, which can be expressed as the negation of the equals operator:

 friend bool operator!=(WEIGHT ls, WEIGHT rs) {return ! (ls==rs);}
But that function doesn't need to be a friend of the Weight class, since it uses no private members. It can be specified after the class definition in the same #include file:
 inline bool operator!=(WEIGHT ls, WEIGHT rs) {return ! (ls==rs);}

By similar reasoning we note that the <=, >, and >= operator functions can be defined like this:

 inline bool operator> (WEIGHT ls, WEIGHT rs) {return   rs  < ls;}
 inline bool operator<=(WEIGHT ls, WEIGHT rs) {return !(ls  > rs);}
 inline bool operator>=(WEIGHT ls, WEIGHT rs) {return   rs <= ls;}

So, we see that if we define the == and < operators as primitive, we can define the other four relational operators in terms of them. That yields much more pleasing answers to the two questions we posed earlier. There's a lot less code to modify if we change our mind, and there's a lot less code to test.

A more serious flaw

In pondering the original question, "What's wrong with this class?" you probably noted that we had chosen a really stupid internal representation for a Weight object. Mixed-base numeric units, such as pounds-and-ounces, may be familiar to some users, but they introduce a lot of complexity in what should be simple.

Therefore, let's change the private internal representation to a simple unit (grams or, if you prefer, ounces), probably floating point, call it value, and simplify the two basic relationals to this:

 friend bool operator== (WEIGHT ls, WEIGHT rs) 
                                     {return ls.value == rs.value;}
 friend bool operator<  (WEIGHT ls, WEIGHT rs) 
                                     {return ls.value <  rs.value;}
What do we now have to do about the other four relational operators? Nothing! If the above two methods are correct, then they're all correct. Simplifying the overloaded operator functions made it much easier to change the internal data representation.

This change to a simpler data representation will also simplify other methods, especially the overloaded arithmentic operators.

Generalizing the pattern

Note that the bodies of the four derived functions know nothing at all about the internal structure of the objects. They refer to no private data. They operate only on their parameters ls and rs.

In fact the bodies of the four derived relational operators would be exactly the same for any class for which ordering (<) and equality (==) are defined. The only difference would be the types of the declared parameters.

But that's exactly what function templates are for. If we define the four derived relationals as function templates, like this:

 template <type=T>
   inline bool operator!=(T& ls, T& rs) {return !(ls == rs); }
 template <type=T>
   inline bool operator> (T& ls, T& rs) {return   rs  < ls;}
 template <type=T>
   inline bool operator<=(T& ls, T& rs) {return !(ls  > rs);}
 template <type=T>
   inline bool operator>=(T& ls, T& rs) {return   rs <= ls;}
They should work for any class that defines equality (==) and ordering (<), and we need never think about defining them again for any new class. There remain, however, a couple of choices to be decided:
  1. Where should we put these function template definitions?

    Anywhere that the compiler will see them. At IDI we collect things that every compilation is likely to need—as if they were extensions to the C++ language— in an #include file "global.hpp".

  2. Should we make them more general by specifying separate types (say T1 and T2) for the two parameters?

    By all means. There may be some situations where you wouldn't want the template version, but you can always define your specialized version. If you learn of a situation where this might lead to trouble, please let me know, so I can amend this advice.

Doesn't everyone do this?

Apparently not. That's why I put this article in the programming peeves section. In programs written by clients and by software vendors and even in a few textbooks, we often encounter:

  1. All six relationals written out in full, like the example at the beginning of this article.

    That's simply poor practice, and should count as a strong indication of programmer naïveté.

  2. Some of the needed relationals omitted altogether.

    Two common reasons lie behind this flaw.

    1. Misguided YAGNIism. The developer needed only < for his own use. and, thinking he was following the advice of agile extremists didn't bother providing >, etc.

    2. Reluctant users of certain other classes, especially library container classes, that invoke operator<. The situation is:
      • The objects of our class are not really ordered. We don't want users of the class to make < comparisons or any of the other ordering relationals.
      • Nevertheless we want to pass objects of our class to a library class that requires operator< for some sort of quasi ordering.
      • For some reason, we can't achieve the result we require by making operator< a private method and conferring friend status on the library class.
      That's an awkward situation, but it doesn't necessarily invalidate our global function templates. It's no harder to refrain from using four operators than to refrain from using one.

Acknowledgment

This article was inspired by a discussion in the Chicago C++ User Group.


Return to IDI Home Page
Return to Technical articles

Last modified 3 July 2012