EXPLORATION 46

image

More Operators

C++ has lots of operators. Lots and lots. So far, I’ve introduced the basic operators that you require for most programs: arithmetic, comparison, assignment, subscript, and function call. Now it’s time to introduce some more: additional assignment operators, the conditional operator (which is like having an if statement in the middle of an expression), and the comma operator (most often used in for loops).

Conditional Operator

The conditional operator is a unique entry in the C++ operator bestiary, being a ternary operator, that is, an operator that takes three operands.

condition ? true-part : false-part

The condition is a Boolean expression. If it evaluates to true, the result of the entire expression is the true-part. If the condition is false, the result is the false-part. As with an if statement, only one part is evaluated; the branch not taken is skipped. For example, the following statement is perfectly safe:

std::cout << (x == 0 ? 0 : y / x);

If x is zero, the y / x expression is not evaluated, and division by zero never occurs. The conditional operator has very low precedence, so you often see it written inside parentheses. A conditional expression can be the source of an assignment expression. So the following expression assigned 42 or 24 to x, depending on whether test is true.

x = test ? 42 : 24;

And an assignment expression can be the true-part or false-part of a conditional expression. That is, the following expression:

x ? y = 1 : y = 2;

is parsed as

x ? (y = 1) : (y = 2);

The true-part and false-part are expressions that have the same or compatible types, that is, the compiler can automatically convert one type to the other, ensuring that the entire conditional expression has a well-defined type. For example, you can mix an integer and a floating-point number; the expression result is a floating-point number. The following statement prints 10.000000 if x is positive:

std::cout << std::fixed << (x > 0 ? 10 : 42.24) << '
';

Do not use the conditional operator as a replacement for if statements. If you have a choice, use an if statement, because a statement is almost always easier to read and understand than a conditional expression. Use conditional expressions in situations when if statements are infeasible. Initializing a data member in a constructor, for example, does not permit the use of an if statement. Although you can use a member function for complicated conditions, you can also use a conditional expression for simple conditions.

The rational class (last seen in Exploration 45), for example, takes a numerator and a denominator as constructor arguments. The class ensures that its denominator is always positive. If the denominator is negative, it negates the numerator and denominator. In past Explorations, I loaded the reduce() member function with additional responsibilities, such as checking for a zero denominator and a negative denominator to reverse the signs of the numerator and denominator. This design has the advantage of centralizing all code needed to convert a rational number to canonical form. An alternate design is to separate the responsibility and let the constructor check the denominator prior to calling reduce(). If the denominator is zero, the constructor throws an exception; if the denominator is negative, the constructor negates the numerator and the denominator. This alternative design makes reduce() simpler, and simple functions are less error prone than complicated functions. Listing 46-1 shows how you can do this using conditional operators.

Listing 46-1.  Using Conditional Expressions in a Constructor’s Initializer

/// Construct a rational object from a numerator and a denominator.
/// If the denominator is zero, throw zero_denominator. If the denominator
/// is negative, normalize the value by negating the numerator and denominator.
/// @post denominator_ > 0
/// @throws zero_denominator
rational::rational(int num, int den)
: numerator_{den < 0 ? -num : num},
  denominator_{den == 0 ? throw zero_denominator("0 denominator") :
                          (den < 0 ? -den : den)}
{
  reduce();
}

A throw expression has type void, but the compiler knows it doesn’t return, so you can use it as one (or both) of the parts of a conditional expression. The type of the overall expression is that of the non-throwing part (or void, if both parts throw an exception).

In other words, if den is zero, the true-part of the expression throws an exception. If the condition is false, the false-part executes, which is another conditional expression, which evaluates the absolute value of den. The initializer for the numerator also tests den, and if negative, it negates the numerator too.

Like me, you might find that the use of conditional expressions makes the code harder to read. The conditional operator is widely used in C++ programs, so you must get used to reading it. If you decide that the conditional expressions are just too complicated, write a separate, private member function to do the work, and initialize the member by calling the function, as shown in Listing 46-2.

Listing 46-2.  Using a Function and Conditional Statements Instead of Conditional Expressions

/// Construct a rational object from a numerator and a denominator.
/// If the denominator is zero, throw zero_denominator. If the denominator
/// is negative, normalize the value by negating the numerator and denominator.
/// @post denominator_ > 0
/// @throws zero_denominator
rational::rational(int num, int den)
: numerator_{den < 0 ? -num : num}, denominator_{init_denominator(den)}
{
  reduce();
}
 
/// Return an initial value for the denominator_ member. This function is used
/// only in a constructor's initializer list.
int rational::init_denominator(int den)
{
  if (den == 0)
    throw zero_denominator("0 denominator");
  else if (den < 0)
    return -den;
  else
    return den;
}

When writing new code, use the technique that you like best, but get used to reading both programming styles.

Short-Circuit Operators

C++ lets you overload the and and or operators, but you must resist the temptation. By overloading these operators, you defeat one of their key benefits: short-circuiting.

Recall from Exploration 12 that the and and or operators do not evaluate their right-hand operands if they don’t have to. That’s true of the built-in operators, but not if you overload them. When you overload the Boolean operators, they become normal functions, and C++ always evaluates function arguments before calling a function. Therefore, overloaded and and or operators behave differently from the built-in operators, and this difference makes them significantly less useful.

image Tip  Do not overload the and and or operators.

Comma Operator

The comma (,) serves many roles: it separates arguments in a function call, parameters in a function declaration, declarators in a declaration, and initializers in a constructor’s initializer list. In all these cases, the comma is a punctuator, that is, a symbol that is part of the syntax that serves only to show where one thing (argument, declarator, etc.) ends and another thing begins. It is also an operator in its own right, which is a completely different use for the same symbol. The comma as operator separates two expressions; it causes the left-hand operand to be evaluated, and then the right-hand operand is evaluated, which becomes the result of the entire expression. The value of the left-hand operand is ignored.

At first, this operator seems a little pointless. After all, what’s the purpose of writing, say,

x = 1 + 2, y = x + 3, z = y + 4

instead of

x = 1 + 2;
y = x + 3;
z = y + 4;

The comma operator is not meant to be a substitute for writing separate statements. There is one situation, however, when multiple statements are not possible, but multiple expressions have to be evaluated. I speak of none other than the for loop.

Suppose you want to implement the search algorithm. Implementing a fully generic algorithm requires techniques that you haven’t learned yet, but you can write this function so that it works with the iterators of a vector<int>. The basic idea is simple, search looks through a search range, trying to find a sequence of elements that are equal to elements in a match range. It steps through the search range one element at a time, testing whether a match starts at that element. If so, it returns an iterator that refers to the start of the match. If no match is found, search returns the end iterator. To check for a match, use a nested loop to compare successive elements in the two ranges. Listing 46-3 shows one way to implement this function.

Listing 46-3.  Searching for a Matching Sub-range in a Vector of Integers

#include <vector>
 
typedef std::vector<int>::iterator viterator;
typedef std::vector<int>::difference_type vdifference;
 
viterator search(viterator first1,viterator last1,viterator first2,viterator last2)
{
  // s1 is the size of the untested portion of the first range
  // s2 is the size of the second range
  // End the search when s2 > s1 because a match is impossible if the remaining
  // portion of the search range is smaller than the test range. Each iteration
  // of the outer loop shrinks the search range by one, and advances the first1
  // iterator. The inner loop searches for a match starting at first1.
  for (vdifference s1(last1-first1), s2(last2-first2); s2 <= s1; --s1, ++first1)
  {
    // Is there a match starting at first1?
    viterator f2(first2);
    for (viterator f1(first1);
         f1 != last1 and f2 != last2 and *f1 == *f2;
         ++f1, ++f2)
     {
        // The subsequence matches so far, so keep checking.
        // All the work is done in the loop header, so the body is empty.
     }
     if (f2 == last2)
       return first1;     // match starts at first1
  }
  // no match
  return last1;
}

The boldface lines demonstrate the comma operator. The initialization portion of the first for loop does not invoke the comma operator. The comma in the declaration is only a separator between declarators. The comma operator appears in the postiteration part of the loops. Because the postiteration part of a for loop is an expression, you cannot use multiple statements to increment multiple objects. Instead, you have to do it in a single expression. Hence, the need for the comma operator.

On the other hand, some programmers prefer to avoid the comma operator, because the resulting code can be hard to read. Rewrite Listing 46-3 so that it does not use the comma operator. Which version of the function do you prefer? ________________ Listing 46-4 shows my version of search without the comma operator.

Listing 46-4.  The search Function Without Using the Comma Operator

#include <vector>
 
typedef std::vector<int>::iterator viterator;
typedef std::vector<int>::difference_type vdifference;
 
viterator search(viterator first1,viterator last1,viterator first2,viterator last2)
{
  // s1 is the size of the untested portion of the first range
  // s2 is the size of the second range
  // End the search when s2 > s1 because a match is impossible if the remaining
  // portion of the search range is smaller than the test range. Each iteration
  // of the outer loop shrinks the search range by one, and advances the first1
  // iterator. The inner loop searches for a match starting at first1.
  for (vdifference s1(last1-first1), s2(last2-first2); s2 <= s1; --s1)
  {
    // Is there a match starting at first1?
    viterator f2(first2);
    for (viterator f1(first1); f1 != last1 and f2 != last2 and *f1 == *f2; )
    {
      ++f1;
      ++f2;
    }
    if (f2 == last2)
      return first1;     // match starts at first1
    ++first1;
  }
  // no match
  return last1;
}

The comma operator has very low precedence, even lower than assignment and the conditional operator. If a loop has to advance objects by steps of 2, for example, you can use assignment expressions with the comma operator.

for (int i{0}, j{size-1}; i < j; i += 2, j -= 2) do_something(i, j);

By the way, C++ lets you overload the comma operator, but you shouldn’t take advantage of this feature. The comma is so basic, C++ programmers quickly grasp its standard use. If the comma does not have its usual meaning, readers of your code will be confused, bewildered, and stymied when they try to understand it.

Arithmetic Assignment Operators

In addition to the usual arithmetic operators, C++ has assignment operators that combine arithmetic with assignment: +=, -=, *=, /=, and %=. The assignment operator x += y is shorthand for x = x + y, and the same applies to the other special assignment operators. Thus, the following three expressions are all equivalent if x has a numeric type:

x = x + 1;
x += 1;
++x;

The advantage of the special assignment operator is that x is evaluated only once, which can be a boon if x is a complicated expression. If data has type std::vector<int>, which of the following two equivalent expressions do you find easier to read and understand?

data.at(data.size() / 2) = data.at(data.size() / 2) + 10;
data.at(data.size() / 2) += 10;

Listing 46-5 shows a sample implementation of *= for the rational class.

Listing 46-5.  Implementing the Multiplication Assignment Operator

rational& rational::operator*=(rational const& rhs)
{
  numerator_ *= rhs.numerator();
  denominator_ *= rhs.denominator();
  reduce();
  return *this;
}

The return type of operator*= is a reference, rational&. The return value is *this. Although the compiler lets you use any return type and value, the convention is for assignment operators to return a reference to the object, that is, an lvalue. Even if your code never uses the return value, many programmers use the result of an assignment, so don’t use void as a return type.

rational r;
while ((r += rational{1,10}) != 2) do_something(r);

Often, implementing an arithmetic operator, such as +, is easiest to do by implementing the corresponding assignment operator first. Then implement the free operator in terms of the assignment operator, as shown in Listing 46-6 for the rational class.

Listing 46-6.  Reimplementing Multiplication in Terms of an Assignment Operator

rational operator*(rational const& lhs, rational const& rhs)
{
  rational result{lhs};
  result *= rhs;
  return result;
}

Implement the /=, +=, and -= operators for class rational. You can implement these operators in many ways. I recommend putting the arithmetic logic in the assignment operators and reimplementing the /, +, and - operators to use the assignment operators, as I did with the multiplication operators. My solution appears in Listing 46-7.

Listing 46-7.  Other Arithmetic Assignment Operators

rational& rational::operator+=(rational const& rhs)
{
  numerator_ = numerator() * rhs.denominator() + rhs.numerator() * denominator();
  denominator_ *= rhs.denominator();
  reduce();
  return *this;
}
 
rational& rational::operator-=(rational const& rhs)
{
  numerator_ = numerator() * rhs.denominator() - rhs.numerator() * denominator();
  denominator_ *= rhs.denominator();
  reduce();
  return *this;
}
 
rational& rational::operator/=(rational const& rhs)
{
  if (rhs.numerator() == 0)
    throw zero_denominator{"divide by zero"};
  numerator_ *= rhs.denominator();
  denominator_ *= rhs.numerator();
  if (denominator_ < 0)
  {
    denominator_ = -denominator_;
    numerator_ = -numerator_;
  }
  reduce();
  return *this;
}

Because reduce() no longer checks for a negative denominator, any function that might change the denominator to negative must check. Because the denominator is always positive, you know that operator+= and operator-= cannot cause the denominator to become negative. Only operator/= introduces that possibility, so only that function needs to check.

Increment and Decrement

Let’s add increment (++) and decrement (--) operators to the rational class. Because these operators modify the object, I suggest implementing them as member functions, although C++ lets you use free functions too. Implement the prefix increment operator for class rational. Compare your function with mine in Listing 46-8.

Listing 46-8.  The Prefix Increment Operator for rational

rational& rational::operator++()
{
  numerator_ += denominator_;
  return *this;
}

I am confident that you can implement the decrement operator with no additional help. Like the arithmetic assignment operators, the prefix operator++ returns the object as a reference.

That leaves the postfix operators. Implementing the body of the operator is easy and requires only one additional line of code. However, you must take care with the return type. The postfix operators cannot simply return *this, because they return the original value of the object, not its new value. Thus, these operators cannot return a reference. Instead, they must return a plain rational rvalue.

But how do you declare the function? A class can’t have two separate functions with the same name (operator++) and arguments. Somehow, you need a way to tell the compiler that one implementation of operator++ is prefix and another is postfix.

The solution is that when the compiler calls a custom postfix increment or decrement operator, it passes the integer 0 as an extra argument. The postfix operators don’t need the value of this extra parameter; it’s just a placeholder to distinguish prefix from postfix.

Thus, when you declare operator++ with an extra parameter of type int, you are declaring the postfix operator. When you declare the operator, omit the name for the extra parameter. That tells the compiler that the function doesn’t use the parameter, so the compiler won’t bother you with messages about unused function parameters. Implement the postfix increment and decrement operators forrational. Listing 46-9 shows my solution.

Listing 46-9.  Postfix Increment and Decrement Operators

rational rational::operator++(int)
{
  rational result{*this};
  numerator_ += denominator_;
  return result;
}
 
rational rational::operator--(int)
{
  rational result{*this};
  numerator_ -= denominator_;
  return result;
}

Once all the dust settles from our rehabilitation project, behold the new, improved rational class definition in Listing 46-10.

Listing 46-10.  The rational Class Definition

#ifndef RATIONAL_HPP_
#define RATIONAL_HPP_
 
#include <iostream>
#include <stdexcept>
#include <string>
 
/// Represent a rational number (fraction) as a numerator and denominator.
class rational
{
public:
  class zero_denominator : public std::logic_error
  {
  public:
    zero_denominator(std::string const& what) : logic_error{what} {}
  };
  rational(): rational{0} {}
  rational(int num): numerator_{num}, denominator_{1} {}
  rational(int num, int den);
  rational(double r);
 
  int numerator()              const { return numerator_; }
  int denominator()            const { return denominator_; }
  float as_float()             const;
  double as_double()           const;
  long double as_long_double() const;
 
  rational& operator=(int); // optimization to avoid an unneeded call to reduce()
  rational& operator+=(rational const& rhs);
  rational& operator-=(rational const& rhs);
  rational& operator*=(rational const& rhs);
  rational& operator/=(rational const& rhs);
  rational& operator++();
  rational& operator--();
  rational operator++(int);
  rational operator--(int);
 
private:
  /// Reduce the numerator and denominator by their GCD.
  void reduce();
  /// Reduce the numerator and denominator, and normalize the signs of both,
  /// that is, ensure denominator is not negative.
  void normalize();
  /// Return an initial value for denominator_. Throw a zero_denominator
  /// exception if @p den is zero. Always return a positive number.
  int init_denominator(int den);
  int numerator_;
  int denominator_;
};
 
/// Compute the greatest common divisor of two integers, using Euclid's algorithm.
int gcd(int n, int m);
 
rational abs(rational const& r);
rational operator-(rational const& r);
rational operator+(rational const& lhs, rational const& rhs);
rational operator-(rational const& lhs, rational const& rhs);
rational operator*(rational const& lhs, rational const& rhs);
rational operator/(rational const& lhs, rational const& rhs);
 
bool operator==(rational const& a, rational const& b);
bool operator<(rational const& a, rational const& b);
 
inline bool operator!=(rational const& a, rational const& b)
{
  return not (a == b);
}
 
inline bool operator<=(rational const& a, rational const& b)
{
  return not (b < a);
}
 
inline bool operator>(rational const& a, rational const& b)
{
  return b < a;
}
 
inline bool operator>=(rational const& a, rational const& b)
{
  return not (b > a);
}
 
std::istream& operator>>(std::istream& in, rational& rat);
std::ostream& operator<<(std::ostream& out, rational const& rat);
 
#endif

The next Exploration is your second project. Now that you know about classes, inheritance, operator overloading, and exceptions, you are ready to tackle some serious C++ coding.

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

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