EXPLORATION 32

image

Assignment and Initialization

The final step needed to complete this stage of the rational type is to write assignment operators and to improve the constructors. It turns out C++ does some work for you, but you often want to fine-tune that work. Let’s find out how.

Assignment Operator

Until now, all the rational operators have been free functions. The assignment operator is different. The C++ standard requires that it be a member function. One way to write this function is shown in Listing 32-1.

Listing 32-1.  First Version of the Assignment Operator

struct rational
{
  rational(int num, int den)
  : numerator{num}, denominator{den}
  {
    reduce();
  }
 
  rational& operator=(rational const& rhs)
  {
    numerator = rhs.numerator;
    denominator = rhs.denominator;
    reduce();
    return *this;
  }
  int numerator;
  int denominator;
};

Several points require further explanation. When you implement an operator as a free function, you need one parameter per operand. Thus, binary operators require a two-parameter function, and unary operators require a one-parameter function. Member functions are different, because the object itself is an operand (always the left-hand operand), and the object is implicitly available to all member functions; therefore, you need one fewer parameter. Binary operators require a single parameter (as you can see in Listing 32-1), and unary operators require no parameters (examples to follow).

The convention for assignment operators is to return a reference to the enclosing type. The value to return is the object itself. You can obtain the object with the expression *this (this is a reserved keyword).

Because *this is the object itself, another way to refer to members is to use the dot operator (e.g., (*this).numerator) instead of the basic numerator. Another way to write (*this).numerator is this->numerator. The meaning is the same; the alternative syntax is mostly a convenience. Writing this-> is not necessary for these simple functions, but it’s often a good idea. When you read a member function, and you have trouble discerning the members from the nonmembers, that’s a signal that you have to help the reader by using this-> before all the member names. Listing 32-2 shows the assignment operator with explicit use of this->.

Listing 32-2.  Assignment Operator with Explicit Use of this->

rational& operator=(rational const& that)
{
  this->numerator = that.numerator;
  this->denominator = that.denominator;
  reduce();
  return *this;
}

The right-hand operand can be anything you want it to be. For example, you may want to optimize assignment of an integer to a rational object. The way the assignment operator works with the compiler’s rules for automatic conversion, the compiler treats such an assignment (e.g., r = 3) as an implicit construction of a temporary rational object, followed by an assignment of one rational object to another.

Write an assignment operator that takes an int parameter. Compare your solution with mine, which is shown in Listing 32-3.

Listing 32-3.  Assignment of an Integer to a rational

rational& operator=(int num)
{
  this->numerator = num;
  this->denominator = 1; // no need to call reduce()
  return *this;
}

If you do not write an assignment operator, the compiler writes one for you. In the case of the simple rational type, it turns out that the compiler writes one that works exactly like the one in Listing 32-2, so there was actually no need to write it yourself (except for instructional purposes). When the compiler writes code for you, it is hard for the human reader to know which functions are actually defined. Also, it is harder to document the implicit functions. So C++ 11 lets you state explicitly that you want the compiler to supply a special function for you, by following a declaration (not a definition) with =default instead of a function body.

rational& operator=(rational const&) = default;

Constructors

The compiler also writes a constructor automatically, specifically one that constructs a rational object by copying all the data members from another rational object. This is called a copy constructor. Any time you pass a rational argument by value to a function, the compiler uses the copy constructor to copy the argument value to the parameter. Any time you define a rational variable and initialize it with the value of another rational value, the compiler constructs the variable by calling the copy constructor.

As with the assignment operator, the compiler’s default implementation is exactly what we would write ourselves, so there is no need to write the copy constructor. As with an assignment operator, you can state explicitly that you want the compiler to supply its default copy constructor.

rational(rational const&) = default;

If you don’t write any constructors for a type, the compiler also creates a constructor that takes no arguments, called a default constructor. The compiler uses the default constructor when you define a variable of a custom type and do not provide an initializer for it. The compiler’s implementation of the default constructor merely calls the default constructor for each data member. If a data member has a built-in type, the member is left uninitialized. In other words, if we did not write any constructors for rational, any rational variable would be uninitialized, so its numerator and denominator would contain garbage values. That’s bad—very bad. All the operators assume the rational object has been reduced to normal form. They would fail if you passed an uninitialized rational object to them. The solution is simple: don’t let the compiler write its default constructor. Instead, you write one.

All you have to do is write any constructor at all. This will prevent the compiler from writing its own default constructor. (It will still write its own copy constructor.)

Early on, we wrote a constructor for the rational type, but it was not a default constructor. As a result, you could not define a rational variable and leave it uninitialized or initialize it with empty braces. (You may have run into that issue when writing your own test program.) Uninitialized data is a bad idea, and having default constructors is a good idea. So write a default constructor to make sure a rational variable that has no initializer has a well-defined value nonetheless. What value should you use? I recommend zero, which is in keeping with the spirit of the default constructors for types such as string and vector. Write a default constructor for rational to initialize the value to zero.

Compare your solution with mine, which is presented in Listing 32-4.

Listing 32-4.  Overloaded Constructors for rational

rational()
: numerator{0}, denominator{1}
{}

Putting It All Together

Before we take leave of the rational type (only temporarily; we will return), let’s put all the pieces together, so you can see what you’ve accomplished over the past four Explorations. Listing 32-5 shows the complete definition of rational and the related operators.

Listing 32-5.  Complete Definition of rational and Its Operators

#include <cassert>
#include <cstdlib>
#include <istream>
#include <ostream>
#include <sstream>
 
/// Compute the greatest common divisor of two integers, using Euclid’s algorithm.
int gcd(int n, int m)
{
  n = std::abs(n);
  while (m != 0) {
    int tmp(n % m);
    n = m;
    m = tmp;
  }
  return n;
}
 
/// Represent a rational number (fraction) as a numerator and denominator.
struct rational
{
  rational()
  : numerator{0}, denominator{1}
  {/*empty*/}
 
  rational(int num)
  : numerator{num}, denominator{1}
  {/*empty*/}
 
  rational(int num, int den)
  : numerator{num}, denominator{den}
  {
    reduce();
  }
 
  rational(double r)
  : rational{static_cast<int>(r * 10000), 10000}
  {/*empty*/}
 
  rational& operator=(rational const& that)
  {
    this->numerator = that.numerator;
    this->denominator = that.denominator;
    reduce();
    return *this;
  }
 
  float as_float()
  {
    return static_cast<float>(numerator) / denominator;
  }
 
  double as_double()
  {
    return static_cast<double>(numerator) / denominator;
  }
 
  long double as_long_double()
  {
    return static_cast<long double>(numerator) /
           denominator;
  }
 
  /// Assign a numerator and a denominator, then reduce to normal form.
  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;
  int denominator;
};
 
/// Absolute value of a rational number.
rational abs(rational const& r)
{
  return rational{std::abs(r.numerator), r.denominator};
}
 
/// Unary negation of a rational number.
rational operator-(rational const& r)
{
  return rational{-r.numerator, r.denominator};
}
 
/// Add rational numbers.
rational operator+(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.denominator + rhs.numerator * lhs.denominator,
                  lhs.denominator * rhs.denominator};
}
 
/// Subtraction of rational numbers.
rational operator-(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.denominator - rhs.numerator * lhs.denominator,
                  lhs.denominator * rhs.denominator};
}
 
/// Multiplication of rational numbers.
rational operator*(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.numerator, lhs.denominator * rhs.denominator};
}
 
/// Division of rational numbers.
/// TODO: check for division-by-zero
rational operator/(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.denominator, lhs.denominator * rhs.numerator};
}
 
/// Compare two rational numbers for equality.
bool operator==(rational const& a, rational const& b)
{
  return a.numerator == b.numerator and a.denominator == b.denominator;
}
 
/// Compare two rational numbers for inequality.
inline bool operator!=(rational const& a, rational const& b)
{
  return not (a == b);
}
/// 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);
}
/// 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);
}
 
/// Read a rational number.
/// Format is @em integer @c / @em integer.
std::istream& operator>>(std::istream& in, rational& rat)
{
  int n{0}, d{0};
  char sep{''};
  if (not (in >> n >> sep))
    // Error reading the numerator or the separator character.
    in.setstate(in.failbit);
  else if (sep != '/')
  {
    // Push sep back into the input stream, so the next input operation
    // will read it.
    in.unget();
    rat.assign(n, 1);
  }
  else if (in >> d)
    // Successfully read numerator, separator, and denominator.
    rat.assign(n, d);
  else
    // Error reading denominator.
    in.setstate(in.failbit);
 
  return in;
}
 
/// Write a rational numbers.
/// Format is @em numerator @c / @em denominator.
std::ostream& operator<<(std::ostream& out, rational const& rat)
{
  std::ostringstream tmp{};
  tmp << rat.numerator << '/' << rat.denominator;
  out << tmp.str();
 
  return out;
}

I encourage you to add tests to the program in Listing 30-5, to exercise all the latest features of the rational class. Make sure everything works the way you expect it. Then put aside rational for the next Exploration, which takes a closer look at the foundations of writing custom types.

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

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