Operator Overloading – Introduction to Boost.Operators, Part 3

This is the third part of my introductory series on Boost.Operators. In the first part and second part I have written about the underlying ideas of the library and provided a basic usage example.

In this post we will further enhance our example `class Rational` by providing support for mixed operations with `Rational` and `double`, having a more detailed look at the facilities provided by Boost.Operators.

Mixed Operations

The implementation of `class Rational` already allowed operations between `Rational` and `int` indirectly, by providing an implicit conversion from `int` to `Rational`. So we can mix `int`s and objects of type `Rational` at will in our calculations, but what happens when we throw in `float` and `double`?

If we for example multiply a `Rational` with a `double`, the compiler will at first find that there is no way to implicitly convert `Rational` into `double`, but there is an implicit conversion from `double` to `Rational` vía `int`. So `Rational(3,2) * 3.14` will be the same as `Rational(3,2) * 3` which is clearly not what we want. So the first thing to do is to disallow this kind of implicit conversion for anything that is not an integral type.

Doing that means splitting up the constructor for `Rational` and using SFINAE for the version with one parameter:

class Rational /* : operator groups... */ {
  /* ... */
public:
  Rational(int n, int d) //the old one, without default parameters
    : numerator( (d>0) ? n: -n )
    , denominator( (d>0) ? d: -d) 
  {
    cancel();
  }

  Rational()
    : numerator( 0 )
    , denominator( 1 ) 
  {}
  
  template <typename T, 
    typename std::enable_if<std::is_integral::value>::type* = nullptr>
  Rational(T n) 
    : numerator{n}
    , denominator{1} 
  {}
};

So for now we have prevented floating point types from wrongly participating in our calculations. But we want them in, so how do we make it right?

Enabling conversions from `double` to `Rational` does not seem a good choice. The finite precision of `double`s would allow it for certain ranges, but converting 3.741e-5 to a `Rational(3741,100000000)` does not seem very useful.

A conversion from `Rational` to `double` could make sense. Any calculation that involves a `double` and a `Rational` then could return a `double`. However, providing an implicit conversion operator to `double` can give us some trouble, since we already have the implicit conversion from integral types to `Rational` and it can become hard to track which implicit conversions may be possible. The compiler can come up with very surprising conversion sequences, so we better stay away from too many implicit conversion possibilities. Therefore we stick to the explicit conversion operator from the last part of this series:

class Rational /* : operator groups */
{
  /* ... */
public:
  explicit operator double() const {
    return static_cast<double>(numerator)/denominator;
  }
};

Mixed Operators in Boost.Operators

Now that we have the foundations for bringing `double` and `Rational` together, can Boost.Operators do anything for us in that regard? The answer is yes.

The library provides a two-type version for most of the templates for binary operators that I presented in the last posts. For example there is a template `addable<T, U>` that provides `T operator+ (T const&, U const&)`, given that `t += u` compiles for corresponding variables `t` and `u`. For symmetric operators both possibilities are generated, i.e. `addable<T, U>` will give us `t+u` and `u+t`.

For asymmetric operators there are two templates: `subtractable<T, U>` will allow `t-u`, and `subtractable2_left<T, U>` gives us `u-t`. However, the latter has an additional requirement that a `T` can be constructed from `u`.

If you have read the last part of this series you might have noticed that `addable<T, U>` for two different operands and `addable<T, B>` for base class chaining look exactly the same. The implementors of the library have used some tricks to distinguish between the two.

Mixed Operators and Automatic Conversions

Before we jump right in and provide the operations between `Rational` and `double` we have to keep in mind that `int` is convertible into `double`, so if `double` gets into the game, we can not rely on the implicit conversion from `int` to `Rational`.

Thet means, we don’t know if `Rational(1,2) + 1`, would result in an addition between `Rational`s or between `Rational` and `double`. So we will have to make the conversion from `int` to `Rational` explicit and implement mixed operations between `Rational` and `int` first, to make them explicitly do the right thing:

class Rational : boost::ordered_euclidian_ring_operators<Rational
               , boost::unit_steppable<Rational
               , boost::equivalent<Rational
               // now mixed operations Rational vs. int:
               , boost::ordered_euclidian_ring_operators<Rational, int
               , boost::equivalent<Rational, int
               > > > > >
{
  /* ... */
  template <class I>
  explicit Rational(I n, 
    typename std::enable_if<std::is_integral<I>::value>::type* = 0
  )
    : numerator( n )
    , denominator( 1 ) 
  {}

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

  Rational& operator*= (int rhs) { 
    return (*this) *= Rational(rhs); 
  } 
  
  Rational& operator/= (int rhs) { 
    return (*this) /= Rational(rhs); 
  } 
}; 

bool operator < (Rational const& lhs, int rhs) { 
  return lhs < Rational(rhs); 
} 

bool operator > (Rational const& lhs, int rhs) { 
  return lhs > Rational(rhs); 
}

That is all we have to add. The `ordered_euclidian_ring_operators` for mixed parameter types contain all mixed operations, including the asymmetric ones like `subtractable2_left`. The only thing that is different to the operations that take only `Rational` as parameters is the necessity for `operator> (Rational const&, int)`.

Rational vs. double

Now, that wasn’t too hard, right? The mixed operations for `Rational` vs `double` should be equally easy to implement. As it turns out, they are, except for some caveats. The target type for those mixed operations should be `double`, so we have to implement the basic operators for `double` instead of `Rational` and instantiate the operator groups in the form `equivalent<double, Rational>`.

Because `double` is not a user defined class we could change, we have to implement `operator+=` etc. as free functions. We can not derive `double` from the operator group templates either, so we will put those into the base class list of `Rational`:

class Rational : boost::ordered_euclidian_ring_operators<Rational
               , boost::unit_steppable<Rational
               , boost::equivalent<Rational
               , boost::ordered_euclidian_ring_operators<Rational, int
               , boost::equivalent<Rational, int
               // new operator groups:
               , boost::ordered_euclidian_ring_operators<double, Rational
               , boost::equivalent<double, Rational
               > > > > > > >
{
};

//free operators for double and Rational
double& operator+= (double& lhs, Rational const& rhs) {
  return lhs += double(rhs);
}

double& operator-= (double& lhs, Rational const& rhs) {
  return lhs -= double(rhs);
}

double& operator*= (double& lhs, Rational const& rhs) {
  return lhs *= double(rhs);
}

double& operator/= (double& lhs, Rational const& rhs) {
  return lhs /= double(rhs);
}

bool operator< (double const& lhs, Rational const& rhs) {
  return lhs < double(rhs);
}

bool operator> (double const& lhs, Rational const& rhs) {
  return lhs > double(rhs);
}

So the lesson learned for the two-type versions of the operator templates is that the order of the template parameters determines the type of the return value: `addable<Rational, int>` produces two `operator+` that return `Rational`s, while `addable<double, Rational>` produce `operator+` that return `double`s.

Conclusion

So that’s it, the development of our `class Rational` is completed. We have implemented quite a few operators in the end, but the seven lines of inheriting some templates have generated 47 additional overloaded operators just like that. Here is the complete class listing of `class Rational`:

#include <boost/operators.hpp>
#include <iostream>
#include <type_traits>

class Rational : boost::ordered_field_operators<Rational 
               , boost::unit_steppable<Rational
               , boost::equivalent<Rational
               , boost::ordered_euclidian_ring_operators<Rational, int
               , boost::equivalent<Rational, int
               , boost::ordered_euclidian_ring_operators<double, Rational
               , boost::equivalent<double, 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:
  Rational(int n, int d)
    : numerator( (d>0) ? n: -n )
    , denominator( (d>0) ? d: -d) 
  {
    cancel();
  }

  Rational()
    : numerator( 0 )
    , denominator( 1 ) 
  {}
  
  template <class I, 
    typename std::enable_if<std::is_integral<I>::value>::type* = nullptr>
  explicit Rational(I n) 
    : numerator{ n }
    , denominator{ 1 } 
  {}

  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;
  }
  
  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;
  }
  
  friend std::ostream& operator<< (std::ostream& os, Rational const& rhs) {
    return os << rhs.numerator << '/' << rhs.denominator;
  }

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

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

  Rational& operator*= (int rhs) {
    return (*this) *= Rational(rhs);
  }

  Rational& operator/= (int rhs) {
    return (*this) /= Rational(rhs);
  }
};

bool operator < (Rational const& lhs, int rhs) { 
  return lhs < Rational(rhs); 
}

bool operator > (Rational const& lhs, int rhs) { 
  return lhs > Rational(rhs); 
}

//free operators for double and Rational
double& operator+= (double& lhs, Rational const& rhs) {
  return lhs += double(rhs);
}

double& operator-= (double& lhs, Rational const& rhs) {
  return lhs -= double(rhs);
}

double& operator*= (double& lhs, Rational const& rhs) {
  return lhs *= double(rhs);
}

double& operator/= (double& lhs, Rational const& rhs) {
  return lhs /= double(rhs);
}

bool operator< (double const& lhs, Rational const& rhs) {
  return lhs < double(rhs);
}

bool operator> (double const& lhs, Rational const& rhs) {
  return lhs > double(rhs);
}

That’s it for the introduction to Boost.Operators. I hope this sheds some light on the possibilities the library provides. I hope I did not bore you too much with operator overloading by now. The next post will be about a completely different topic.

Previous Post
Next Post

5 Comments


  1. I don’t like the enable_if part being part of the function signature. The converting constructor from an itegral type to Rational should take 1 argument and not 2. So make enable_if a template parameter. Then one problem remains: If we pass for example 1ul (type unsigned long int) to this constructor, we get a narrowing conversion. Using braces (uniform initialization) solves this problem too.

    template <typename T, typename std::enable_if<std::is_integral::value>::type* = nullptr>
    Rational(T n) : numerator_{n}, denominator_{1} {}

    Reply

    1. Thank you for the input, I fixed it.

      Reply

      1. In the first code snippet (top of the page),

        typename std::enable_if .....

        should be

        typename std::enable_if&lt;std::is_integral::value&gt;

        Reply

        1. Sorry, my prev. comment was somehow mis-parsed. You are missing the template T for std::is_integral in the first code snippet.

          Reply

Leave a Reply

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