18.2 Operator Functions

I l @ ve RuBoard

Using the add function for fixed-point numbers is a little awkward . It would be nice to be able to convince C++ to automatically call this function whenever we try to add two fixed numbers together with the + operator. That's where operator overloading comes in. All we have to do is to turn the add function into a function named operator + (technically you can't have a space in a function name , but this is a special case):

 inline fixed_pt operator + (const fixed_pt& oper1,                              const fixed_pt& oper2) {   return fixed_pt(oper1.value + oper2.value); } 

When C++ sees that the fixed_pt class has a member function named operator + it automatically generates code that calls it when two fixed-point objects are added.

The operator overloading functions should be used carefully . You should try to design them so they follow common-sense rules. That is, + should have something to do with addition; -, with subtraction; and so on. The C++ I/O streams break this rule by defining the shift operators ( << and >> ) as input and output operators. This can lead to some confusion, such as:

 std::cout << 8 << 2; 

Does this output "8" followed by "2," or does it output the value of the expression ( 8 << 2 )? Unless you're an expert, you can't tell. In this case, the numbers "8" and "2" will be output.

You've seen how you can overload the + operator. Now let's explore what other operators you can use.

18.2.1 Binary Arithmetic Operators

Binary operators take two arguments, one on each side of the operator. For example, multiplication and division are binary operators:

 x * y; a / b; 

Unary operators take a single parameter. Unary operators include unary - and the address-of ( & ) operator:

 -x &y 

The binary arithmetic operator functions take two constant parameters and produce a result. One of the parameters must be a class or structure object. The result can be anything. For example, the following functions are legal for binary addition:

 fixed_pt operator +(const fixed_pt& v1, const fixed_pt& v2); fixed_pt operator +(const fixed_pt& v1, const float v2); fixed_pt operator +(const float v1,     const fixed_pt& v2); fixed_pt operator +(float v1,    float v2);               // Illegal // You can't overload a basic C++ operator such as adding two floating // point numbers. 

It makes sense to add real (float) and fixed-point numbers together. The result is that we've had to define a lot of different functions just to support the addition for our fixed-point class. Such variation of the definition is typical when overloading operators.

Table 18-2 lists the binary operators that can be overloaded.

Table 18-2. Binary operators that can be overloaded

Operator

Meaning

+

Addition

-

Subtraction

*

Multiplication

/

Division

%

Modulus

^

Bitwise exclusive OR

&

Bitwise AND

Bitwise OR

<<

Left shift

>>

Right shift

18.2.2 Relational Operators

The relational operators include such operators as equal ( == ) and not equal ( != ). Normally they take two constant objects and return either true or false. (Actually they can return anything, but that would violate the spirit of relational operators.)

The equality operator for our fixed-point class is:

 inline bool operator == (const fixed_pt& oper1, const fixed_pt& oper2) {     return (oper1.value == oper2.value); } 

Table 18-3 lists the relational operators.

Table 18-3. Relational operators

Operator

Meaning

==

Equality

!=

Inequality

<

Less than

>

Greater than

<=

Less than or equal to

>=

Greater than or equal to

18.2.3 Unary Operators

Unary operators, such as negative (-), take a single parameter. The negative operator for our fixed-point type is:

 inline fixed_pt operator - (const fixed_pt& oper1, const double oper2) {   return fixed_pt(oper1.value - fixed_pt::double_to_fp(oper2)); } 

Table 18-4 lists the unary operators.

Table 18-4. Unary operators

Operator

Meaning

+

Positive

-

Negative

*

Dereference

&

Address of

~

Ones complement

18.2.4 Shortcut Operators

Operators such as += and -= are shortcuts for more complicated operators. But what are the return values of += and -=? A very close examination of the C++ standard reveals that these operators return the value of the variable after the increase or decrease. For example:

 i = 5; j = i += 2;    // Don't code like this 

assigns j the value 7 . The += function for our fixed-point class is:

 inline fixed_pt& operator += (fixed_pt& oper1,                                const fixed_pt& oper2)  {     oper1.value += oper2.value;     return (oper1); } 

Note that unlike the other operator functions we've defined, the first parameter is not a constant. Also, we return a reference to the first variable, not a new variable or a copy of the first.

Table 18-5 lists the shortcut operators.

Table 18-5. Simple shortcut operators

Operator

Meaning

+=

Increase

-=

Decrease

*=

Multiply by

/=

Divide by

%=

Remainder

^=

Exclusive OR into

&=

AND into

=

OR into

<<=

Shift left

>>=

Shift right

18.2.5 Increment and Decrement Operators

The increment and decrement operators have two forms: prefix and postfix. For example:

 i = 5; j = i++;    // j = 5 i = 5; j = ++i;    // j = 6 

Both these operators use a function named operator ++ . So how do you tell them apart? The C++ language contains a hack to handle this case. The prefix form of the operator takes one argument, the item to be incremented. The postfix takes two, the item to be incremented and an integer. The actual integer used is meaningless; it's just a position holder to differentiate the two forms of the operation.

Our functions to handle the two forms of ++ are:

 // Prefix      x = ++f inline fixed_pt& operator ++(fixed_pt& oper)  {     oper.value += fixed_exp;     return (oper); } // Postfix     x = f++ inline fixed_pt operator ++(fixed_pt oper, int)  {     fixed_pt result(oper);   // Result before we incremented     oper.value += fixed_exp;     return (result); } 

This is messy. C++ has reduced us to using cute tricks: the unused integer parameter. In actual practice, I never use the postfix version of increment and always put the prefix version on a line by itself. That way, I can avoid most of these problems.

The choice, prefix versus postfix, was decided by looking at the code for the two versions. As you can see, the prefix version is much simpler than the postfix version. Restricting yourself to the prefix version not only simplifies your code, but it also makes the compiler's job a little easier.

Table 18-6 lists the increment and decrement operators.

Table 18-6. Increment and decrement operators

Operator

Meaning

++

Increment

--

Decrement

18.2.6 Logical Operators

Logical operators include AND ( && ), OR ( ), and NOT ( ! ). They can be overloaded, but just because you can do it doesn't mean you should. In theory, logical operators work only on bool values. In practice, because numbers can be converted to the bool type (0 is false, nonzero is true), these operators work for any number. But don't confuse the issue more by overloading them.

Table 18-7 lists the logical operators.

Table 18-7. Logical operators

Operation

Meaning

Logical OR

&&

Logical AND

!

Logical NOT

18.2.7 I/O Operators

You've been using the operators << and >> for input and output. Actually these operators are overloaded versions of the shift operators. This has the advantage of making I/O fairly simple, at the cost of some minor confusion.

We would like to be able to output our fixed-point numbers just like any other data type. To do this we need to define a << operator for it.

We are sending our data to the output stream class std:: ostream . The data itself is fixed_pt . So our output function is:

 inline std::ostream& operator << (std::ostream& out_file,                                    const fixed_pt& number) {     long int before_dp = number.value / fixed_exp;     long int after_dp1  = abs(number.value % fixed_exp);     long int after_dp2  = after_dp1 % 10;     after_dp1 /= 10;     out_file << before_dp << '.' << after_dp1 << after_dp2;     return (out_file); } 

The function returns a reference to the output file. This enables the caller to string a series of << operations together, such as:

 fixed_pt a_fixed_pt(1.2); std::cout << "The answer is " << a_fixed_pt << '\n'; 

The result of this code is:

 The answer is 1.20 

Normally the << operator takes two constant arguments. In this case, the first parameter is a nonconstant std::ostream . This is because the << operator, when used for output, has side effects, the major one being that the data goes to the output stream. In general, however, it's not a good idea to add side effects to an operator that doesn't already have them.

Input should be just as simple as output. You might think all we have to do is read the numbers (and the related extra characters ):

 // Simple-minded input operation inline istream& operator >> (istream& in_file, fixed_pt& number) {     int before_dp;       // Part before the decimal point     char dot;            // The decimal point     char after_dp1;      // After decimal point (first digit)     char after_dp2;      // After decimal point (second digit)     in_file >> before_dp >> dot >> after_dp1 >> after_dp2;     number.value = before_dp * fixed_exp +                    after_dp1 - '0' * 10 +                    after_dp2 - '0';     return (in_file); } 

In practice, it's not so simple. Something might go wrong. For example, the user may type in 1x23 instead of 1.23. What do we do then?

The answer is that we would fail gracefully. In our new reading routine, the first thing we do is set the value of our number to 0.00. That way, if we do fail, there is a known value in the number:

 inline std::istream& operator >> (std::istream& in_file,                                    fixed_pt& number) {     number.value = 0; 

Next we create a std::istream::sentry variable. This variable protects the std::istream in case of failure:

 std::istream::sentry the_sentry(in_file, true); 

The second parameter tells the sentry to skip any leading whitespace. (It's optional. The default value is false, which tells the sentry to not skip the whitespace.)

Now we need to check to see if everything went well when the sentry was constructed :

 if (the_sentry) {     // Everything is OK, do the read    .... } else {     in_file.setstate(std::ios::failbit); // Indicate failure } 

The function setstate is used to set a flag indicating that the input operation found a problem. This allows the caller to test to see whether the input worked by calling the bad function. (This function can also cause an exception to be thrown. See Chapter 22 for more information.)

Let's assume that everything is OK. We've skipped the whitespace at the beginning of the number, so we should now be pointing to the digits in front of the decimal point. Let's grab them. Of course we check for errors afterwards:

 in_file >> before_dp;   // Get number before the decimal point if (in_file.bad(  )) return (in_file); 

The next step is to read the decimal point, make sure that nothing went wrong, and that we got the decimal point:

 in_file >> ch;  // Get first character after number if (in_file.bad(  )) return (in_file); // Expect a decimal point if (ch != '.') {    in_file.setstate(std::ios::failbit);    return (in_file); } 

Now we get the two characters after the decimal point and check for errors:

 in_file >> after_dp1 >> after_dp2; if (in_file.bad(  )) return (in_file); 

Both characters should be digits (we're not very flexible in our input format ”that's a feature, not a bug). To make sure that the correct characters are read, we use the standard library function isdigit to check each of them to make sure they are digits. (See your library documentation for information on isdigit and related functions.)

 // Check result for validity if ((!isdigit(after_dp1))  (!isdigit(after_dp2))) {    in_file.setstate(std::ios::failbit);    return (in_file); } 

Everything is OK at this point, so we set the number and we're done:

 // Todo make after db two digits exact number.value = before_dp * fixed_exp +     (after_dp1 - '0') * 10 +     (after_dp2 - '0'); 

The complete version of the fixed-point number reader appears in Example 18-1.

Example 18-1. fixed_pt/fixed_pt.read
 /********************************************************  * istream >> fixed_pt -- read a fixed_pt number        *  *                                                      *  * Parameters                                           *  *      in_file -- file to read                         *  *      number -- place to put the number               *  *                                                      *  * Returns                                              *  *      reference to the input file                     *  ********************************************************/ std::istream& operator >> (std::istream& in_file, fixed_pt& number) {     long int before_dp; // Part before decimal point (dp)     char after_dp1, after_dp2;  // Part after decimal point (dp)     char ch;            // Random character used to verify input     number = 0.0;       // Initialize the number (just in case)     // We only work for 2 digit fixed point numbers     assert(fixed_exp == 100);     // Sentry to protect the I/O     std::istream::sentry the_sentry(in_file, true);          if (the_sentry)     {         if (in_file.bad(  )) return (in_file);             // Get the number that follows the whitespace         in_file >> before_dp;         if (in_file.bad(  )) return (in_file);         in_file >> ch;  // Get first character after number         if (in_file.bad(  )) return (in_file);         // Expect a decimal point         if (ch != '.') {            in_file.setstate(std::ios::failbit);            return (in_file);         }         in_file >> after_dp1 >> after_dp2;         if (in_file.bad(  )) return (in_file);         // Check result for validity         if ((!isdigit(after_dp1))  (!isdigit(after_dp2))) {            in_file.setstate(std::ios::failbit);            return (in_file);         }         // Todo make after db two digits exact         number.value = before_dp * fixed_exp +             (after_dp1 - '0') * 10 +             (after_dp2 - '0');     }     else     {        in_file.setstate(std::ios::failbit);     }    return (in_file); } 

18.2.8 Index Operator "[ ]"

The operator [ ] is used by C++ to index arrays. As you will see in Chapter 20, this operator is very useful when defining a class that mimics an array. Normally, this function takes two arguments, a class that simulates an array and an index, and returns a reference to an item in the array:

 double& operator[](array_class& array, int index) 

We cover the [] operator in more detail in Chapter 23.

18.2.9 new and delete

We'll say very little about overloading the global operators new and delete at this time. First of all, they aren't introduced until Chapter 20, so you don't know what they do. Second, when you know what they do, you won't want to override them.

I've seen only one program where the new and delete operators (or at least their C equivalents) were overridden. That program was written by a very clever programmer who liked to do everything a little strangely. The result was code that was a nightmare to debug.

So unless you are a very clever programmer, leave new and delete alone. And if you are a clever programmer, please leave new and delete alone anyway. Some day I might have to debug your code.

18.2.10 Exotic Operators

C++ contains a very rich set of operators. Some of these are rarely, if ever, used. These include:

( )

Allows you to define a default function for a class.

,

Comma operator. Allows two expressions to be concatenated . It is rarely used and probably should not be overloaded.

->*

Pointer to member. Rarely used.

->

Class member.

All of these operators are discussed in Chapter 29.

I l @ ve RuBoard


Practical C++ Programming
Practical C Programming, 3rd Edition
ISBN: 1565923065
EAN: 2147483647
Year: 2003
Pages: 364

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net