EXPLORATION 30

image

Overloading Operators

This Exploration continues the study of writing custom types. An important aspect of making a custom type behave seamlessly with built-in types is ensuring that the custom types support all the expected operators—arithmetic types must support arithmetic operators, readable and writable types must support I/O operators, and so on. Fortunately, C++ lets you overload operators in much the same manner as overloading functions.

Comparing Rational Numbers

In the previous Exploration, you began to write a rational type. After making a modification to it, an important step is testing the modified type, and an important aspect of internal testing is the equality (==) operator. C++ lets you define a custom implementation for almost every operator, provided at least one operand has a custom type. In other words, you can’t redefine integer division to yield a rational result, but you can define division of an integer by a rational number, and vice versa.

To implement a custom operator, write a normal function, but for the function name, use the operator keyword, followed by the operator symbol, as shown in the code excerpt in Listing 30-1.

Listing 30-1.  Overloading the Equality Operator

/// Represent a rational number.
struct rational
{
  /// Construct a rational object, given a numerator and a denominator.
  /// Always reduce to normal form.
  /// @param num numerator
  /// @param den denominator
  /// @pre denominator > 0
  rational(int num, int den)
  : numerator{num}, denominator{den}
  {
    reduce();
  }
 
  /// Assign a numerator and a denominator, then reduce to normal form.
  /// @param num numerator
  /// @param den denominator
  /// @pre denominator > 0
  void assign(int num, int den)
  {
    numerator = num;
    denominator = den;
    reduce();
  }
 
  /// Reduce the numerator and denominator by their GCD.
  void reduce()
  {
    assert(denominator != 0);
    if (denominator < 0)
    {
      denominator = -denominator;
      numerator = -numerator;
    }
    int div{gcd(numerator, denominator)};
    numerator = numerator / div;
    denominator = denominator / div;
  }
 
  int numerator;     ///< numerator gets the sign of the rational value
  int denominator;   ///< denominator is always positive
};
 
/// Compare two rational numbers for equality.
/// @pre @p a and @p b are reduced to normal form
bool operator==(rational const& a, rational const& b)
{
  return a.numerator == b.numerator and a.denominator == b.denominator;
}
 
/// Compare two rational numbers for inequality.
/// @pre @p a and @p b are reduced to normal form
bool operator!=(rational const& a, rational const& b)
{
  return not (a == b);
}

One of the benefits of reducing all rational numbers is that it makes comparison easier. Instead of checking whether 3/3 is the same as 6/6, the constructor reduces both numbers to 1/1, so it is just a matter of comparing the numerators and denominators. Another trick is defining != in terms of ==. There’s no point in your making extra work for yourself, so confine the actual logic of comparing rational objects to one function and call it from another function. If you worry about the performance overhead of calling an extra layer of functions, use the inline keyword, as shown in Listing 30-2.

Listing 30-2.  Using inline for Trivial Functions

/// Compare two rational numbers for equality.
/// @pre @p a and @p b are reduced to normal form
bool operator==(rational const& a, rational const& b)
{
  return a.numerator == b.numerator and a.denominator == b.denominator;
}
 
/// Compare two rational numbers for inequality.
/// @pre @p a and @p b are reduced to normal form
inline bool operator!=(rational const& a, rational const& b)
{
  return not (a == b);
}

The inline keyword is a hint to the compiler that you would like the function expanded at the point of call. If the compiler decides to heed your wish, the resulting program will not have any identifiable function named operator!= in it. Instead, every place where you use the != operator with rational objects, the function body is expanded there, resulting in a call to operator==.

To implement the < operator, you need a common denominator. Once you implement operator<, you can implement all other relational operators in terms of <. You can choose any of the relational operators (<, >, <=, >=) as the fundamental operator and implement the others in terms of the fundamental. The convention is to start with <. Listing 30-3 demonstrates < and <=.

Listing 30-3.  Implementing the < Operator for rational

/// Compare two rational numbers for less-than.
bool operator<(rational const& a, rational const& b)
{
  return a.numerator * b.denominator < b.numerator * a.denominator;
}
 
/// Compare two rational numbers for less-than-or-equal.
inline bool operator<=(rational const& a, rational const& b)
{
  return not (b < a);
}

Implement > and >= in terms of <.

Compare your operators with Listing 30-4.

Listing 30-4.  Implementing the > and >= Operators in Terms of <

/// Compare two rational numbers for greater-than.
inline bool operator>(rational const& a, rational const& b)
{
  return b < a;
}
 
/// Compare two rational numbers for greater-than-or-equal.
inline bool operator>=(rational const& a, rational const& b)
{
  return not (b > a);
}

Then write a test program. To help you write your tests, download the test.hpp file and add #include "test.hpp" to your program. Call the TEST() function as many times as you need, passing a Boolean expression as the sole argument. If the argument is true, the test passed. If the argument is false, the test failed, and the TEST function prints a suitable message. Thus, you may write tests, such as the following:TEST(rational{2, 2} == rational{5, 5});TEST(rational{6,3} > rational{10, 6});

The all-capital name, TEST, tells you that TEST is different from an ordinary function. In particular, if the test fails, the text of the test is printed as part of the failure message. How the TEST function works is beyond the scope of this book, but it’s useful to have around; you’ll be using it for future test harnesses. Compare your test program with Listing 30-5.

Listing 30-5.  Testing the rational Comparison Operators

#include <cassert>
#include <cstdlib>
#include <iostream>
#include "test.hpp"
 
... struct rational omitted for brevity ...
 
int main()
{
  rational a{60, 5};
  rational b{12, 1};
  rational c{-24, -2};
  TEST(a == b);
  TEST(a >= b);
  TEST(a <= b);
  TEST(b <= a);
  TEST(b >= a);
  TEST(b == c);
  TEST(b >= c);
  TEST(b <= c);
  TEST(a == c);
  TEST(a >= c);
  TEST(a <= c);
 
  rational d{109, 10};
  TEST(d < a);
  TEST(d <= a);
  TEST(d != a);
  TEST(a > d);
  TEST(a >= d);
  TEST(a != d);
 
  rational e{241, 20};
  TEST(e > a);
  TEST(e >= a);
  TEST(e != a);
  TEST(a < e);
  TEST(a <= e);
  TEST(a != e);
}

Arithmetic Operators

Comparison is fine, but arithmetic operators are much more interesting. You can overload any or all of the arithmetic operators. Binary operators take two parameters, and unary operators take one parameter. You can choose any return type that makes sense. Listing 30-6 shows the binary addition operator and the unary negation operator.

Listing 30-6.  Addition Operator for the rational Type

rational operator+(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.denominator + rhs.numerator * lhs.denominator,
                  lhs.denominator * rhs.denominator};
}
 
rational operator-(rational const& r)
{
  return rational{-r.numerator, r.denominator};
}

Write the other arithmetic operators: -, *, and /. Ignore for the moment the issue of division by zero. Compare your functions with mine, which are presented in Listing 30-7.

Listing 30-7.  Arithmetic Operators for the rational Type

rational operator-(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.denominator - rhs.numerator * lhs.denominator,
                  lhs.denominator * rhs.denominator};
}
 
rational operator*(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.numerator, lhs.denominator * rhs.denominator};
}
 
rational operator/(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.denominator, lhs.denominator * rhs.numerator};
}

Adding, subtracting, etc. with rational numbers is fine, but more interesting is the issue of mixing types. For example, what is the value of 3 * rational(1, 3)? Try it. Collect the definition of the rational type with all the operators and write a main() function that computes that expression and stores it somewhere. Choose a type for the result variable that makes sense to you, then determine how best to print that value to std::cout.

Do you expect the expression to compile without errors? ________________

What is the result type of the expression? ________________

What value do you expect as the result? ________________

Explain your observations.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

It turns out that rational’s one-argument constructor tells the compiler it can construct a rational from an int anytime it needs to do so. It does so automatically, so the compiler sees the integer 3 and a multiplication of an int and a rational object. It knows about operator* between two rationals, and it knows it cannot use the built-in * operator with a rational operand. Thus, the compiler decides its best response is to convert the int to a rational (by invoking rational{3}), and then it can apply the custom operator* that multiplies two rational objects, yielding a rational result, namely, rational{1, 1}. It does all this automatically on your behalf. Listing 30-8 illustrates one way to write the test program.

Listing 30-8.  Test Program for Multiplying an Integer and a Rational Number

#include <cassert>
#include <cstdlib>
#include <iostream>
 
... struct rational omitted for brevity ...
 
int main()
{
  rational result{3 * rational{1, 3}};
  std::cout << result.numerator << '/' << result.denominator << ' ';
}

Being able to construct a rational object automatically from an int is a great convenience. You can easily write code that performs operations on integers and rational numbers without concerning yourself with type conversions all the time. You’ll find this same convenience when mixing integers and floating-point numbers. For example, you can write 1+2.0 without having to perform a type cast: static_cast<double>(1)+2.0.

On the other hand, all this convenience can be too convenient. Try to compile the following code sample and see what your compiler reports.

int a(3.14); // which one is okay,
int b{3.14}; // and which is an error?

You have seen that parentheses are needed when initializing an auto declaration, and we use curly braces everywhere else. In truth, you can use parentheses for many of these initializations, but you pay a cost in safety. The compiler allows conversions that lose information, such as floating-point to integer, when you use parentheses for initialization, but it reports an error when you use curly braces.

The difference is critical for the rational type. Initializing a rational number using a floating-point number in parentheses truncates the number to an integer and uses the one-argument form of constructor. This isn’t what you want at all. Instead, initializing rational{3.14} should produce the same result as rational{314, 100}.

Writing a high-quality conversion from floating-point to a fraction is beyond the scope of this book. Instead, let’s just pick a reasonable power of 10 and use that as the denominator. Say we choose 100000, then rational{3.14159} would be treated as rational{static_cast<int>(3.14159 * 10000), 10000}. Write the constructor for a floating-point number. I recommend using a delegating constructor; that is, write the floating-point constructor so it invokes another constructor.

Compare your result with mine in Listing 30-9. A better solution uses numeric_limits to determine the number of decimal digits of precision double can support, and tries to preserve as much precision as possible. An even better solution uses the radix of the floating-point implementation, instead of working in base 10.

Listing 30-9.  Constructing a Rational Number from a Floating-Point Argument

struct rational
{
  rational(int num, int den)
  : numerator{num}, denominator{den}
  {
    reduce();
  }
 
  rational(double r)
  : rational{static_cast<int>(r * 10000), 10000}
  {}
 
  ... omitted for brevity ...
 
  int numerator;
  int denominator;
};

If you want to optimize a particular function for a particular argument type, you can do that too, by taking advantage of ordinary function overloading. You’d better make sure it’s worth the extra work, however. Remember that the int operand can be the right-hand or left-hand operand, so you will have to overload both forms of the function, as shown in Listing 30-10.

Listing 30-10.  Optimizing Operators for a Specific Operand Type

rational operator*(rational const& rat, int mult)
{
  return rational{rat.numerator * mult, rat.denominator};
}
 
inline rational operator*(int mult, rational const& rat)
{
  return rat * mult;
}

In such a simple case, it’s not worth the added trouble to avoid a little extra arithmetic. However, in more complicated situations, such as division, you may have to write such code.

Math Functions

The C++ standard library offers a number of mathematical functions, such as std::abs, which computes absolute values (as you have already guessed). The C++ standard prohibits you from overloading these standard functions to operate on custom types, but you can still write functions that perform similar operations. In Exploration 52, you’ll learn about namespaces, which will enable you to use the real function name. Whenever you write a custom numeric type, you should consider which math functions you should provide. In this case, absolute value makes perfect sense. Write an absolute value function that works with rational numbers. Call it absval.

Your absval function should take a rational parameter by value and return a rational result. As with the arithmetic operators I wrote, you may opt to use call-by-reference for the parameter. If so, make sure you declare the reference to be const. Listing 30-11 shows my implementation of absval.

Listing 30-11.  Computing the Absolute Value of a Rational Number

rational absval(rational const& r)
{
  return rational{std::abs(r.numerator), r.denominator};
}

That was easy. What about the other math functions, such as sqrt, for computing square roots? Most of the other functions are overloaded for floating-point arguments. If the compiler knew how to convert a rational number to a floating-point number automatically, you could simply pass a rational argument to any of the existing floating-point functions, with no further work. So, which floating-point type should you use? ________________.

This question has no easy answer. A reasonable first choice might be double, which is the “default” floating-point type (e.g., floating-point literals have type double). On the other hand, what if someone really wants the extra precision long double offers? Or what if that person doesn’t need much precision and prefers to use float?

The solution is to abandon the possibility of automatic conversion to a floating-point type and instead offer three functions that explicitly compute the floating-point value of the rational number. Write as_float, as_double, and as_long_double. Each of these member functions computes and returns the floating-point approximation for the rational number. The function name identifies the return type. You will have to cast the numerator and denominator to the desired floating-point type using static_cast, as you learned in Exploration 25. Listing 30-12 shows how I wrote these functions, with a sample program that demonstrates their use.

Listing 30-12.  Converting to Floating-Point Types

struct rational
{
  float as_float()
  {
    return static_cast<float>(numerator) / denominator;
  }
 
  double as_double()
  {
    return numerator / static_cast<double>(denominator);
  }
 
  long double as_long_double()
  {
    return static_cast<long double>(numerator) /
           static_cast<long double>(denominator);
  }
 
... omitted for brevity ...
 
};
 
int main()
{
  rational pi{355, 113};
  rational bmi{90*100*100, 180*180}; // Body-mass index of 90 kg, 180 cm
  double circumference{0}, radius{10};
 
  circumference = 2 * pi.as_double() * radius;
  std::cout << "circumference of circle with radius " << radius << " is about "
            << circumference << ' ';
  std::cout << "bmi = " << bmi.as_float() << ' ';
}

As you can see, if one argument to / (or any other arithmetic or comparison operator) is floating-point, the other operand is converted to match. You can cast both operands or just one or the other. Pick the style that suits you best and stick with it.

One more task would make it easier to write test programs: overloading the I/O operators. That is the topic for the next Exploration.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset