13.3 Overloaded Operators

Team-Fly

13.3 Overloaded Operators

The overloading of operators represents a powerful mechanism that makes it possible to define functions with the same name but with different parameter lists, functions that can then carry out differing operations. The compiler uses the specified parameter list to determine which function is actually meant. To make this possible C++ employs strong type-checking, which tolerates no ambiguity or inconsistency.

The overloading of operator functions makes it possible to use the "normal" way of expressing a sum c = a + b with LINT objects a, b, and c instead of having to invoke a function like, for example, add_l(a_l, b_l, c_l). This enables the seamless integration of our class into the programming language and significantly improves the readability of programs. For this example it is necessary to overload both the operator "+" and the assignment "=".

There are only a few operators in C++ that cannot be overloaded. Even the operator "[]", which is used for access to vectors, can be overloaded, for example by a function that simultaneously checks whether the access to a vector oversteps the vector's bounds. However, please note that the overloading of operators opens the door to all possible mischief. To be sure, the effect of the operators of C++ on the standard data types cannot be altered; nor can the predefined precedence order of the operators (cf. [Str1], Section 6.2) be changed or new operators "created." But for individual classes it is fully possible to define operator functions that have nothing in common with what one traditionally has associated with the operator as it is normally employed. In the interest of maintainability of programs one is well advised to stick close to the meaning of the standard operators in C++ when overloading operators if one is to avoid unnecessary confusion.

One should note in the above outline of the LINT class that certain operators have been implemented as friend functions and others as member functions. The reason for this is that we would like, for example, to use "+" or "*" as two-position operators that can not only process two equivalent LINT objects but accept alternatively one LINT object and one of the built-in C++ integer types, and moreover, accept the arguments in either order, since addition is commutative. To this end we require the above-described constructors, which create LINT objects of out integer types. Mixed expressions such as in

   LINT a, b, c;   int number;   // Initialize a, b, and number and calculate something or other   // ...   c = number * (a + b / 2) 

are thus possible. The compiler takes care of calling the appropriate constructor functions automatically and sees to it that the transformation of the integer type number and the constant 2 into LINT objects takes place at run time, before the operators + and * are invoked. We thereby obtain the greatest possible flexibility in the application of the operators, with the restriction that expressions containing objects of type LINT are themselves of type LINT and can thereafter be assigned only to objects of type LINT.

Before we get ourselves involved in the details of the individual operators, we would like to give an overview of the operators defined by the class LINT, for which the reader is referred to Tables 13.2 through 13.5,

Table 13.2: LINT arithmetic operators

+

addition

++

increment (prefix and postfix operators)

subtraction

--

decrement (prefix and postfix operators)

*

multiplication

/

division (quotient)

%

remainder

We now would like to deal with the implementation of the operator functions "*", "=", "*=", and "==", which may serve as examples of the implementation

Table 13.3: LINT bitwise operators

&

bitwise AND

|

bitwise OR

^

bitwise exclusive OR (XOR)

<<

shift left

>>

shift right

Table 13.4: LINT logical operators

==

equality

!=

inequality

<, <=

less than, less than or equal to

>, >=

greater than, greater than or equal to

Table 13.5: LINT assignment operators

=

simple assignment

+=

assignment after addition

=

assignment after subtraction

*=

assignment after multiplication

/=

assignment after division

%=

assignment after remainder

&=

assignment after bitwise AND

|=

assignment after bitwise OR

^=

assignment after bitwise XOR

<<=

assignment after left shift

>>=

assignment after right shift

of the LINT operators. First, with the help of the operator "*=" we see how multiplication of LINT objects is carried out by the C function mul_l(). The operator is implemented as a friend function, to which both factors associated with the operation are passed as references. Since the operator functions do not change their arguments, the references are declared as const:

 const LINT operator* (const LINT& lm, const LINT& ln) {   LINT prd;   int error; 

start sidebar

The first step is to query the operator function as to whether the arguments lm and ln passed by reference have been initialized. If this is not the case for both arguments, then error handling goes into effect, and the member function panic(), declared as static, is called (cf. Chapter 15).

end sidebar

   if (!lm.init) LINT::panic (E_LINT_VAL, "*", 1, __LINE__);   if (!ln.init) LINT::panic (E_LINT_VAL, "*", 2, __LINE__); 

start sidebar

The C function mul_l() is called, to which are passed as arguments the vectors lm.n_l, ln.n_l as factors, as well as prd.n_l for storing the product.

end sidebar

   error = mul_l (lm.n_l, ln.n_l, prd.n_l); 

start sidebar

The evaluation of the error code stored in error distinguishes three cases: If error == 0, then all is right with the world, and the object prd can be marked as initialized. This takes place by setting the variables prd.init to 1. The status variable prd.status was already set by the constructor to the value E_LINT_OK. If an over-flow occurred with mul_l(), then error contains the value E_CLINT_OFL. Since the vector prd.n_l contains in this case a valid CLINT number, prd.init is set to 1, while the status variable prd.status contains the value E_LINT_OFL. If error has neither of these two values after the call to mul_l(), then something has gone awry in these functions without our being able to identify more precisely what error has occurred. In this case the function panic() is called for further error handling.

end sidebar

   switch (error)     {       case 0:          prd.init = 1;          break;       case E_CLINT_OFL:          prd.status = E_LINT_OFL;          prd.init = 1;          break;       default:          lint::panic (E_LINT_ERR, "*", error, __LINE__);     } 

start sidebar

If the error cannot be repaired by panic(), there would be no point in returning to this location. The mechanism for error recognition leads here to a defined termination, which in principle is better than continuing the program in an undefined state. As a final step we have the elementwise return of the product prd.

end sidebar

     return prd;   } 

Since the object prd exists only within the context of the function, the compiler makes sure that a temporary object is created automatically, which represents the value of prd outside the function. This temporary object is generated with the aid of the copy constructor LINT(const LINT&) (cf. page 265) and exists until the expression within which the operator was used has been processed, that is, until the closing semicolon has been reached. Due to the declaration of the function value as const such nonsensical constructs as (a * b) = c; will not get past the compiler. The goal is to treat LINT objects in exactly the same way as the built-in integer types.

We can extend the operator functions by the following detail: If the factors to be multiplied are equal, then the multiplication can be replaced by squaring, so that the advantage in efficiency associated with this changeover can be utilized automatically (cf. Section 4.2.2). However, since in general it costs an elementwise comparison of the arguments to determine whether they are equal, which is too expensive for us, we shall be satisfied with a compromise: Squaring will be brought into play only if both factors refer to one and the same object. Thus we test whether ln and lm point to the same object and in this case execute the squaring function instead of multiplication. Here is the relevant code:

   if (&lm == &ln)     {       error = sqr_l (lm.n_l, prd.n_l);     }   else     {       error = mul_l (lm.n_l, ln.n_l, prd.n_l);     } 

This falling back on the functions implemented in C from Part I is a model for all of the remaining functions of the class LINT, which is formed like a shell around the kernel of C functions and protects it from the user of the class.

Before we turn our attention to the more complex assignment operator "*=", it seems a good idea to take a closer look at the simple assignment operator "=". Already in Part I we established that assignment of objects requires particular attention (cf. Chapter 8). Therefore, just as in the C implementation we had to pay heed that in assigning one CLINT object to another the content and not the address of the object was assigned, we must likewise for our LINT class define a special version of the assignment operator "=" that does more than simply copy elements of the class: For the same reasons as were introduced in Chapter 8 we must therefore take care that it is not the address of the numerical vector n_l that is copied, but the digits of the numerical representation pointed to by n_l.

Once one has understood the fundamental necessity for proceeding thus, the implementation is no longer particularly complicated. The operator "=" is implemented as a member function, which returns as a result of the assignment a reference to the implicit left argument. Of course, we use internally the C function cpy_l() to move digits from one object into the other. For executing the assignment a = b the compiler calls the operator function "=" in the context of a, where a takes over the role of an implicit argument that is not given in the parameter list of the operator function. Within the member function reference to the elements of the implicit argument is made simply by naming them without context. Furthermore, a reference to the implicit object can be made via the special pointer this, as in the following implementation of the operator "=":

   const LINT& LINT::operator= (const LINT& ln)   {     if (!ln.init) panic (E_LINT_VAL, "=", 2, __LINE__);     if (maxlen < DIGITS_L (ln.n_l))       panic (E_LINT_OFL, "=" , 1, __LINE__); 

start sidebar

First, a check is made as to whether the references to the right and left arguments are identical, since in this case copying is unnecessary. Otherwise, the digits of the numerical representation of ln are copied into those of the implied left argument *this, just as the values of init and status, and with *this the reference to the implicit argument is returned.

end sidebar

     if (&ln != this)       {          cpy_l (n_l, ln.n_l);          init = 1;          status = ln.status;       }     return *this;   } 

One might ask whether the assignment operator must necessarily return any value at all, since after LINT::operator =(const LINT&) is called the intended assignment appears to have been accomplished. However, the answer to the question is clear if one recalls that expressions of the form

 f (a = b); 

are allowed. According to the semantics of C++, such an expression would result in a call to the function f with the result of the assignment a = b as argument. Thus is it imperative that the assignment operator return the assigned value as result, and for reasons of efficiency this is done by reference. A special case of such an expression is

 a = b = c; 

where the assignment operator is called two times, one after the other. At the second call the result of the first assignment b = c is assigned to a.

In contrast to the operator "*", the operator "*=" changes the leftmost of the two passed factors by overwriting it with the value of the product. The meaning of the expression a *= b as an abbreviated form of a = a * b should, of course, remain true for LINT objects. Therefore, the operator "*=" can, like the operator "=", be set up as a member function that for the reasons given above returns a reference to the result:

 const LINT& LINT::operator*= (const LINT& ln) {     int error;     if (!init) panic (E_LINT_VAL, "*=", 0, __LINE__);     if (!ln.init) panic (E_LINT_VAL, "*=", 1, __LINE__);     if (&ln == this)       error = sqr_l (n_l, n_l);     else       error = mul_l (n_l, ln.n_l, n_l);     switch (error)       {          case 0:            status = E_LINT_OK;            break;          case E_CLINT_OFL:            status = E_LINT_OFL;            break;          default:            panic (E_LINT_ERR, "*=", error, __LINE__);          }   return *this;   } 

As our last example of a LINT operator we shall describe the function "==", which tests for the equality of two LINT objects: As result the value 1 is returned in the case of equality, and otherwise 0. The operator == also illustrates the implementation of other logical operators.

   const int operator == (const LINT& lm, const LINT& ln)   {     if (!lm.init) LINT::panic (E_LINT_VAL, "==", 1, __LINE__);     if (!ln.init) LINT::panic (E_LINT_VAL, "==", 2, __LINE__);     if (&ln == &lm)       return 1;     else       return equ_l (lm.n_l, ln.n_l);   } 


Team-Fly


Cryptography in C and C++
Cryptography in C and C++
ISBN: 189311595X
EAN: 2147483647
Year: 2001
Pages: 127

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