Operator Overloading – Introduction to Boost.Operators, Part 2

This is the second part of my introduction to Boost.Operators. Click here for the first part. I will start right where I stopped in the last part.

“Do as the ints do”… (continued)

Operator Groups

The different operator families I have written about in the last part are further combined into opeator groups. Boost distinguishes between arithmetic and iterator related operator groups. The user can choose between using the groups and manually combining the families, on modern compilers the result is the same.

Arithmetic Operator GrOups

Usually it makes sense to have more than one operator family for a given type. For example, if you can add two objects, you can often subtract them as well. Number types like `class Rational` from the last post have all four basic arithmetic operations.

To facilitate the definition of operators for such classes, boost defines templates for the operator groups that are very similar to the ones for the operator families. For example, the group `ordered_field_operators` contains the families `addable`, `subtractable`, `multiplicable`, `dividable`, `less_than_comparable` and `equality_comparable`. The names speak for themselves.

For the arithmetic operators there are sometimes two groups with different names that contain the same operator families. This is due to dfferent points of view of the groups: One can either just join the groups of the basic arithmetic operations or use a group theory point of view.

The smallest groups for the basic arithmetic operations are `additive` (`addable` and `subtractable` families joined) and `multiplicative` (`multipliable` and `dividable`). Those two groups together form the group `arithmetic` and contain all four basic operations. In addition there are the groups `integer_multipliable` and `integer_arithmetic`, where the modulo operation (family `modable`) is joined to the `multipliable` and `arithmetic` group, respectively.

group name operations (in addition to corresponding +=, *= etc.)
`additive` +, –
`multiplicative` *, /
`arithmetic` +, – *, /
`integer_multiplicative` *, /, %
`integer_arithmetic` +, -, *, /, %

The group theory side looks like follows: The group `additive` and the family `multipliable` from the group `ring_operators`. Joining it with division we get `field_operators`, and adding modulo operation to that we have `euclidian_ring_operators`. The comparison families `less_than_comparable` and `equality_comparable` form the group `totally_ordered`. Adding this one to the group theory groups, we get `ordered_ring_operators`, `ordered_field_operators` and `ordered_euclidian_ring_operators`.

group name operations (in addition to corresponding +=, *= etc.)
`ring_operators` +, -, *
`field_operators` +, -, *, /
`euclidian_ring_operators` +, – *, /, %
`totally_ordered` ==, < etc.
`ordered_ring_operators` +, -, *, ==, < etc.
`ordered_field_operators` +, -, *, /, ==, < etc.
`ordered_euclidian_ring_operators` +, – *, /, %, ==, < etc.

In addition to all those groups there are three smaller operator groups:

group name operations
`bitwise` &, |, ^, &=, |=, ^=
`unit_steppable` ++, — (both pre and post)
`shiftable` <<, >>, <<=, >>=

Iterator Operations and Iterator Helpers

Similar to the arithmetic groups there are operator groups that contain the operations of the usual iterator categories defined in the standard. The names speak for themselves: `input_iteratable`, `output_iteratable`, `forward_iteratable`, `bidirectional_iteratable` and `random_access_iteratable`. `input_iteratable` and `forward_iteratable` both contain the same operations (dereferencing, increments, equality), however the names show that they are meant to be used in different contexts.

group name operations
`output_iteratable` ++
`input_iteratable` ->, ++, ==
`forward_iteratable` ->, ++, ==
`bidirectional_iteratable` ->, ++, –, ==
`random_access_iteratable` ->, [], +, -, ++, –, ==, < etc.

In addition the library provides a so called operator helper for each of the operator groups, that contains the group and the typedefs demanded by the standard for iterators, like `value_type`, `difference_type` and `iterator_category`. Those helpers are named `input_iterator_helper`, `forward_iterator_helper` and so on.

Using Boost.Operators

Now that we have digged ourseves through the theory and some details what the library can do, let’s get to work and have a look at the basic usage. I will use `class Rational` again, the example from the first part of this series.

Class Rational From the Start.

Let’s start by putting together what we need to represent a rational number.

  • We keep it simple by having two `int`s as members, representing numerator and denominator.
  • We acquire no resources or responsibilities of any kind by creating a `Rational`, so we write no destructor and no copy or move operations.
  • Constructors that we could need are the default constructor that should zero-initialize the object, one for providing numerator and denominator, adn one to convert from `int` to rational.
  • We keep it simple again, by not providing a conversion constructor from float or double to Rational, however we provide a conversion to double. The conversion operator should be `explicit` to avoid trouble with implicit conversions and the built in operations for double.
  • We want out numerator and denominator to be as small as possible, so we assume we have a function to cancel the fraction. Another invariant should be that only the numerator may be negative.
  • For simplicity, we won’t check for division by zero and integer overflows – this is a little sandbox example after all 😉
class Rational {
  //invariants:
  //- the fraction is always canceled as far as possible
  //- the denominator is always positive, i.e. only the numerator is signed
  int numerator;
  int denominator;

  void cancel(); //left as exercise for the reader

public:
  //constructors: three in one - default and implicit int conversion included
  Rational(int n = 0, int d = 1)
    : numerator( (d>0) ? n: -n )
    , denominator( (d>0) ? d: -d) 
  {
    cancel();
  }

  Rational operator- () const {
    auto tmp = *this;
    tmp.numerator *= -1;
    return tmp;
  }

  Rational operator+ () const {
    return *this;
  }

  Rational invert() const {
    return Rational(denominator, numerator);
  }

  explicit operator double() const {
    return static_cast<double>(numerator)/denominator;
  }
};

Next comes the implementation of the arithmetic basic operations. As I had explained in the last post, Boost.Operators need `operator+=` to generate `operator+` and so on. We also add increment and decrement operators as well as comparisons.

class Rational {
/* ... see above ...*/
public:

  Rational& operator+= (Rational const& rhs) {
    numerator *= rhs.denominator;
    numerator += denominator * rhs.numerator;
    denominator *= rhs.denominator;
    cancel();
    return *this;
  }

  Rational& operator-= (Rational const& rhs) {
    *this += (-rhs);
    return *this;
  }

  Rational& operator*= (Rational const& rhs) {
    numerator *= rhs.numerator ;
    denominator*= rhs.denominator;
    cancel();
    return *this;
  }

  Rational& operator/= (Rational const& rhs) {
    *this *= rhs.invert();
    return *this;
  }

  Rational& operator++() {
    numerator += denominator;
    return *this;
  }

  Rational& operator--() {
    numerator -= denominator;
    return *this;
  }

  friend bool operator< (Rational const& lhs, Rational const& rhs) {
    return lhs.numerator * rhs.denominator < rhs.numerator * lhs.denominator;
  }
};

That is all we need to let Boost.Operators do the rest.

Rational Meets Boost

If we look at the table of operator families in the last post and compare it with the operators we have implemented, we can identify the following operator families we can use:

  • `addable`, `subtractable`, `multipliable` and `dividable`
  • `incrementable` and `decrementable`
  • `less_than_comparable` and `equivalent`, which enables us to use `equality_comparable`

To use each of those families for our class we have two possibilities: We can either have Rational inherit from each of them or use a technique that is called “base class chaining”. The inheritance can be public, protected or private, it has no influence on the outcome.

//multiple inheritance, flat hierarchy:

class Rational : boost::addable<Rational>
               , boost::subtractable<Rational> 
               , boost::multipliable<Rational>
               , boost::dividable<Rational>
               , boost::incrementable<Rational>
               , boost::decrementable<Rational>
               , boost::less_than_comparable<Rational>
               , boost::equivalent<Rational>
               , boost::equality_comparable<Rational>
{
/*...*/
};

//base class chaining:
class Rational : boost::addable<Rational
                 , boost::subtractable<Rational
                   , boost::multipliable<Rational
                     , boost::dividable<Rational
                       , boost::incrementable<Rational
                         , boost::decrementable<Rational
                           , boost::less_than_comparable<Rational
                             , boost::equivalent<Rational
                               , boost::equality_comparable<Rational> 
                             > 
                           > 
                         > 
                       > 
                     > 
                   > 
                 > 
               >
{
/*...*/
};

That looks a bit scary. The first version uses ninefold inheritance, the second a ninefold nested template. Base class chaining means that we derive from one template, where the second parameter is a base class for that template, which is another of the templates and so on. So the uppermost class  is `equality_comparable`, inherited by `equivalent` etc. The base class chaining technique should be preferred, because it allows the Empty Base Optimization since all of those templates don’t have any data members.

This big bunch of templates can be reduced if we use operator groups. The groups are templates like the families, so using them is straight forward:

class Rational : boost::ordered_field_operators<Rational 
               , boost::unit_steppable<Rational
               , boost::equivalent<Rational> > >
{
/*...*/
};

So these three lines generate eleven additional operators, and we have everything to compare and calculate among Rational objects. Since all the binary operators generated by Boost are free functions and since we have the implicit conversion constructor from int, we can also calculate between Rational and int.

Rational half(1, 2);
auto oneAndAHalf = 1 + half;
assert(oneAndHalf * 2 == 3);

Conclusion

As you see, Boost can make operator overloading fairly easy. With only little effort we could provide a complete set of operators for `class Rational`.

In the next and last post of the series I will demonstrate how Boost.Operators supports mixed operators by providing support for mixeed operations with `double` for our `class Rational`.

Facebooktwittergoogle_plusredditlinkedinFacebooktwittergoogle_plusredditlinkedinby feather

6 Comments



  1. I think that “base class chaining” is very ugly and unnecessary syntax for C++11.
    It could be easy replaced by more friendly syntax:

    warp_base_class

    where `warp_base_class` is template alias that hide all ugly parts.
    Core implementation is simple:

    template<typename T,
    template F,
    template… Rest>
    warp_base_class_helper
    {
    using type = typename F<T typename warp_base_class_helper>::type;
    }

    Reply

    1. Hi Yankes, thanks for dropping by!
      I am not sure I completely understand what the template would achieve. I tried to compile it and got several errors – I assume you meant `typename F` and `typename… Rest`, but after that I must confess I am lost: I have no idea how to fix this last error (link).

      Reply

        1. Ok, I get your point now. That technique would allow to write something like this: `class Rational : public boost::operators<Rational, addable, subtractable, multipliable>`, and if it gets a bit enhanced that template could even distinguish between the one- and two- parameter versions (see the part three of the series).

          Reply

Leave a Reply