Tải bản đầy đủ - 0 (trang)
Chapter 10. Operator Functions: Another Good idea

Chapter 10. Operator Functions: Another Good idea

Tải bản đầy đủ - 0trang

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



the tremendous progress in hardware power resulted in a dramatic increase in the size of computer

memory and in the execution speed of most computers. This placated whatever anxiety was

remaining about C++ memory requirements and run-time performance. Experience with C++

clearly demonstrated that programming with classes can be efficient. Any new programming

language that will be developed in the near future is expected to support classes.

Misgivings about robustness were not placated at all. Just the other way around: Industrial

experience confirmed the dangers and pitfalls that programmers should be aware of. Strangely, this

did not prevent C++ from becoming a major programming language for a broad spectrum of

applications. The complexity of the language is the major contributing factor to the failure to

achieve robustness. In the previous chapter, you saw that the idea behind C++ classes is simple.

C++ classes had to help the programmers

ϒΠ



to bind together object data and operations



ϒΠ



to control access to class elements from the outside of the class



ϒΠ



to introduce additional scope for avoiding name conflicts and



ϒΠ



to push responsibilities from clients to servers



The previous chapter also showed you that C++ designer Bjarne Stroustroup put into C++ classes

much more than this list of four items requires. Constructors and destructors help class objects

manage their resources, mostly dynamically allocated memory. The availability of these member

functions puts a burden on the class programmer (server designer) to provide a variety of

constructors to support client code in a variety of contexts. They also put some additional burden

on client programmers for supplying data for object initialization, but this is considered a minor

side effect.

Using composite objects results in additional complications. The designer of the container class

should facilitate initialization of component objects. The member initialization list provides a new

syntax for doing that. The idea of composite classes requires incorporation of such additional

details as constant components, reference components, pointer components, and recursive

components. The concept of class attributes leads to other extensions of this idea such as static data

members and static functions that characterize the class as a whole rather than as individual object

instances of the class.

I also mentioned that C++ has yet another design goal: treating the class instances in the same way

as the variables of built-in types. In the previous chapter, this principle manifested itself in the form

of the uniform syntax for initialization for both objects and variables. In this chapter, I will discuss

yet another manifestation of the same idea: extending it to C++ operators so that the same

expression syntax with operators can be applied to class objects in the same way as it is applied to

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (538 of 1187) [8/17/2002 2:57:56 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



variables of built-in types in conventional C++ expressions.

As usual, C++ supports more than one way to do that. I will discuss different techniques for

implementing overloaded operator functions. These techniques will help you become more

proficient in using C++ and in understanding what is going on under the hood of a C++ program.



Overloading of Operators

In C++, the concept of programmer-defined types (classes) is an extension of the concept of builtin numeric types. You can define variables of programmer-defined types using the same syntax as

for simple numeric variables. Similar to built-in types, you can use object instances of programmerdefined types as array elements or as data members of even more-complex types. You can pass

objects of programmer-defined types as parameters and return them from functions. You can set

pointers and references to programmer-defined values using the same syntax as for built-in values.

You can define pointers as constant pointers. You can define pointers and references as pointers

and references to constant values using the same syntax as for built-in types.

These similarities are not accidental. One of the important C++ goals was to treat programmerdefined types in the same way as it treats built-in types. This goal has nothing to do with objectoriented programming, improving productivity of development, enhancing efficiency of

maintenance, or any other software engineering consideration. This is a purely aesthetic goal. And

this is legitimate. Computer programming, as any creative human activity, has an essential aesthetic

component. Although programming books rarely discuss this issue, the programs we write should

be as elegant as they should be readable, portable, and maintainable.

Of course, many programs, especially large programs, are not elegant. Neither are they readable,

portable, or maintainable, but the language is designed to help the programmer achieve these goals.

There is, however, a big gap in treating classes and numeric types in the same way. C++

programmer-defined types are not exactly like native numeric types. The biggest difference is that

you cannot apply C++ operators to objects of programmer-defined types¡Xaddition, subtraction,

comparisons for equality, inequality, and so on. You can write your own functions to implement

these operations, and notation might often be somewhat awkward.

Let us consider a simple example: complex numbers that are characterized by values of the real and

imaginary components. Those of you who are not familiar with complex numbers could think of

them as points on the plane where the real component corresponds to the x-coordinate, and the

imaginary component corresponds to the y-coordinate. When you add (or subtract) complex

numbers, the result is yet another complex number; its real component is the result of adding (or

subtracting) the real components of the two operands, and its imaginary component is the result of

adding (or subtracting) the imaginary components of the two operands. Multiplication and division

are more complicated, but they also are carried as operations over components of the operands.

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (539 of 1187) [8/17/2002 2:57:56 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



Let us represent complex numbers as a class with two data members, real and imag. For

simplicity of the discussion, I will leave both data members public (they will become private in the

next version of the class).

struct Complex {

double real, imag; } ;



// public data members



Listing 10.1 shows an example of the code that defines object instances of type Complex,

initializes them, and then performs some arithmetic operations over these objects.

NOTE

This is not a good example of C++ code. Most C++ books avoid showing bad C++ code. As a

result, the reader never learns how to see the difference between bad and good C++ code. This is

pretty much like learning painting by going to great museums rather than taking art lessons.

Similar to painting, C++ programming is always a struggle to find a solution that is better than a

competing solution is. Instead of showing you a reasonable solution, I prefer to show you an

inferior solution, explain what is wrong with it and how it could be improved, and then show you a

better solution and explain why this solution is better.



In Listing 10.1, the client code performs computations over complex numbers by using direct

access to public object components: The client code specifies the names of data members real and

imag instead of using access functions. As a result, the client code represents the mix of access to

data fields and the computations over data field values. The meaning of these computations is not

expressed in the function calls and has to be deduced by the maintainer from the analysis of lowlevel details of computations. The responsibility for low-level operations is not pushed down to

server functions, and the developer has to keep in mind several levels of the algorithm

simultaneously: the high-level goal of the computation and its low-level details. There are no

separate areas of concern, and changes to the design of class Complex will affect the client code as

well. The use of the keyword struct instead of class is appropriate here since all data members

are public.



Example 10.1. Example of operations over objects of class Complex.

#include

using namespace std;



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (540 of 1187) [8/17/2002 2:57:56 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



struct Complex {

double real, imag; } ;



// programmer-defined type



int main()

{ Complex x, y, z1, z2;

// objects of type Complex

x.real = 20; x.imag = 40;

// initialization

y.real = 30; y.imag = 50;

cout << " First value: ";

cout << "(" << x.real << ", " << x.imag << ")" << endl;

cout << " Second value: ";

cout << "(" << y.real << ", " << y.imag << ")" << endl;

z1.real = x.real + y.real;

// add real components into z1

z1.imag = x.imag + y.imag;

// add imaginary components

cout << " Sum of two values:

";

cout << "(" << z1.real << ", " << z1.imag << ")" << endl;

z2.real = x.real + y.real;

// add real components into z2

z2.imag = x.imag + y.imag;

// add imaginary components

z1.real = z1.real + x.real;

// add to the real component of z1

z1.imag = z1.imag + x.imag;

// add to the imag component of z1

cout << " Add first value to z1: ";

cout << "(" << z1.real << ", " << z1.imag << ")" << endl;

z2.real += 30.0;

// add to real component of z2

cout << " Add 30 to sum:

";

cout << "(" << z2.real << ", " << z2.imag << ")" << endl;

return 0;

}



The output of the run of this program is shown in Figure 10-1.



Figure 10-1. Output for program in Listing 10.1.



Although this is not a good example of object-oriented programming, it is a good starting point for

the discussion of operator function overloading. Also, I'd like to take the opportunity to repeat the

list of drawbacks of poor use of C++. This list is very important: Repeatedly using it for the

evaluation of your code is the best way to learn how to use C++ correctly and how to improve the

quality of your C++ code.

To encapsulate the client code from the details of data design, you have to write access functions

that would manipulate the objects of type Complex to serve the needs of client code. For example,

if you want to add variables of this type, you have to write a function that accepts two parameters

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (541 of 1187) [8/17/2002 2:57:56 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



of type Complex, performs necessary computations over components of these two objects, and

returns the result as a value of the same type. This means that the interface of this function named,

for example, addComplex(), will look this way:

Complex addComplex(const Complex &a, const Complex &b);



As I mentioned earlier, adding two complex values requires adding their real components and

adding their imaginary components.

Complex addComplex(const Complex &a, const Complex &b)

{ Complex c;

// local object

c.real = a.real + b.real;

// add real components

c.imag = a.imag + b.imag;

// add imaginary components

return c; }



To use this function, the client code defines and initializes variables of type Complex, passes them

as parameters to this function, and uses its return value as a value of type Complex.

Complex x, y, z1, z2;

x.real = 20; x.imag = 40;

y.real = 30; y.imag = 50;

z1 = addComplex(x,y);



// objects of type Complex

// initialization

// use in the function call



This is very nice (and trivial). Most programmers are used to this functional style of programming

and do not feel that using function names like addComplex() makes their programs ugly or

unreadable. A real C++ programmer, however, feels uncomfortable (to say the least) that C++ does

not support (at least, not yet), writing the client code in the following way.

Complex x, y, z1, z2;

x.real = 20; x.imag = 40;

y.real = 30; y.imag = 50;

z2 = x + y;



// objects of type Complex

// initialization

// use in expression



If you do this, the compiler will tell you that the operation of addition is not defined, C++

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (542 of 1187) [8/17/2002 2:57:56 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



ambitions for equal treatment of types notwithstanding. Since you cannot use built-in operations on

programmer-defined data types, you have to invent new function names like addComplex() and

implement them to perform necessary operations. This disparity between programmer-defined

types and built-in types is painful for every real C++ programmer.

As a remedy, C++ offers you a break or, rather, a contract. You, as a programmer, limit yourself to

special function names that include the keyword operator and the sign for the operation you

would like to use in your code, for example, +. You design and implement that function,

operator+(), in exactly the same way you design and implement any function with the name of

your free choice, for example, addComplex(). C++, as a supporting programming language,

allows you to call this function using the operator notation that corresponds to the sign of the

operator you included in the name of the function. If you called the function operator+(), then

you can call this function using the same notation as that for built-in numeric types.

z = x + y;



// under the hood, this is z = operator+(x,y);



Isn't that nice? You associate services provided by the function with a built-in C++ operation. This

is marvelous!

Actually, this is not that unique. In C++, the same function name in the same scope can represent

different algorithms provided their signatures are different (see Chapter 7, "Programming with C++

Functions," for more discussion on function name overloading). When the client code calls the

function, the compiler matches the actual argument types with function declarations available in

that scope and decides which one, if any, to use to implement the function call.

This is true of any C++ function name. As far as arithmetic operators are concerned, operator

overloading is used in every programming language, not only in C++. Operator overloading means

giving multiple interpretations to the same symbol. Consider, for example, the addition operator.

int a,b,c;

float d,e,f;

a = 20; b = 30; d = 40.0; e = 50.0;

c = a + b; f = d + e;

// different operations, same operator



In C++ (and in other languages), the + operator is used to add integer or floating point values.

These operations are very different. For integers, each bit of the first operand is added with each bit

of the second operand and with the carry bit from the lower order bit.

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (543 of 1187) [8/17/2002 2:57:57 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



For floating point values, the binary representation consists of the mantissa and the exponent. To

avoid the complexity of binary (or hex) values, I will illustrate this issue using the example in the

decimal system. In the mantissa-exponent representation, for example, 3000.0 is 3*10^3 and 300.0

is 3*10^2. (Here, I use the operator ^ to denote exponentiation, even though C++ does not have an

exponentiation operator.) When adding floating point values, the mantissa of the smaller operator is

shifted to the right so that the exponents of the two operands became the same (when adding 3000

and 300, 300 would be shifted three decimal positions to the right to be represented as 0.3*10^3).

After that the bits of the mantissa (not all bits as for integers) are added up (when adding 3000 and

300, the result would be 3.3*10^3).

Whatever the details of floating point addition are, it is clear that they are different from the details

of integer addition. At the assembly language level, these operations are represented by two

different operation codes. In high-level programming languages, we do not force the programmers

to learn separate notation for integer addition and for floating point addition.

I hope that you recognize in this discussion the concepts of information hiding and pushing

responsibilities down from client code to server code. In this case, the server is the addition

operator, and the client is the high-level code that contains expressions with the addition operator.

The programmer who writes the expression with the addition operator does not want to know the

details of addition¡Xthis programmer concentrates on the higher goals of the expression and on

related issues. It is the programmer who implements the addition operator that is aware of details of

addition for each type and implements each operator accordingly.

C++ takes this idea of having different operators denoted with the same symbol to the next level

and extends this capability to programmer-defined data types. If you write your functions in

agreement with C++ rules, you can apply any operator (well, with few exceptions) to any

programmer-defined type!

Here is the operator+() function implemented for parameters of the type Complex.

Complex operator+(const Complex &a, const Complex &b)

{ Complex c;

c.real = a.real + b.real;

components

c.imag = a.imag + b.imag;

components

return c; }



// magic name

// local object

// add real

// add imag



How did I write this function? I copied the addComplex() function presented earlier, kept the body

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (544 of 1187) [8/17/2002 2:57:57 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



of the function, return type and the parameter list without changes, cut out the function name

addComplex, and moved in the magic function name operator+. Bingo! I did my part of the deal.

Now C++ will do its part of the deal: it will accept the addition operator with operands of Complex

type and will not print the syntax error message that says that the addition operator is not defined

for the type Complex. This operator is now defined.

Complex x, y, z;

x.real = 20; x.imag = 40;

y.real = 30; y.imag = 50;

z = x + y;



// objects of type Complex

// initialization

// use in expression



When I say that the compiler "will accept the addition operator with operands of Complex type,"

what does this actually mean? What code will the compiler generate? The compiler will call the

overloaded function operator+() that I wrote. It will use the left operand of the expression as the

first actual argument of the function, and the right operand as the second argument of the function.

The code that the compiler will generate for the code snippet above will be exactly the same as for

the following client code.

Complex x, y, z;

x.real = 20; x.imag = 40;

y.real = 30; y.imag = 50;

z = operator+(x,y);



// objects of type Complex

// initialization

// this is absolutely legitimate



If the function name includes the keyword operator and the symbol of the operator, the compiler

accepts either the function call syntax or the operator syntax and will generate exactly the same

code. If you use the function call syntax z=operator+(x,y); the compiler matches the actual

argument types with parameter types as for any other function call. If you use the operator syntax

z=x+y; the compiler discovers that the operands are of a programmer-defined type and searches for

a function whose name contains the keyword operator and the operator sign used in the client

code. If this function is found, the compiler checks whether its parameters match the number and

the types of operands in the client expression.

As a result, the responsibility is pushed down to the server classes, and the client code is purged

from the details of server design. The client programmer uses the same syntax for adding integers,

floating point values, objects of type Complex or whatever other programmer-defined type you

would like to use with this syntax.



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (545 of 1187) [8/17/2002 2:57:57 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



This is a very flexible and powerful mechanism. As with many things in C++, it gives you more

than you bargained for. We started with the goal of using the objects of programmer-defined types

in the same way as the variables of built-in types and wound up with something much more potent.

Now you can do to the objects of your type what you could not even dream of doing to built-in

numeric types because the language does not limit you in what you can do in the privacy of your

own overloaded operator function. You are limited only in the function interface¡Xthe function

name and the number of parameters. These cannot be chosen arbitrarily; they have to emulate the

built-in operator you are overloading.

Listing 10.2 illustrates the use of operator function overloading. In addition to the overloaded

addition operator, it also shows the use of operator+=() that adds one Complex object to another.

It also demonstrates the use of operator+=() that adds a floating-point number to the real part of

the Complex object. Although the names of these two operator functions are the same, their

parameter lists are different. This is a legitimate use of function name overloading (see Chapter 7

for more details on function name overloading in C++).



Example 10.2. Example of operator function overloading.

#include

using namespace std;

struct Complex {

double real, imag; } ;



// programmer-defined type



Complex operator+(const Complex &a, const Complex &b)

// magic name

{ Complex c;

// local object

c.real = a.real + b.real;

// add real components

c.imag = a.imag + b.imag;

// add imaginary components

return c; }

void operator += (Complex &a, const Complex &b)

{ a.real = a.real + b.real;

a.imag = a.imag + b.imag; }



// another magic name

// add to the real component

// add to the imag component



void operator += (Complex &a, double b)

{ a.real += b; }



// different interface

// add to real component only



void showComplex(const Complex &x)

{ cout << "(" << x.real << ", " << x.imag << ")" << endl; }

int main()

{ Complex x, y, z1, z2;

x.real = 20; x.imag = 40;

y.real = 30; y.imag = 50;

cout << " First value:



";



// objects of type Complex

// initialization



showComplex(x);



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (546 of 1187) [8/17/2002 2:57:57 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



cout << " Second value: "; showComplex(y);

z1 = operator+(x,y);

// use in the function call

cout << " Sum as function call:

"; showComplex(z1);

z2 = x + y;

// use as the operator

cout << " Sum as the operator:

"; showComplex(z1);

z1 += x;

// same as operator+=(z1,x);

cout << " Add first value to sum: "; showComplex(z1);

z2 += 30.0;

// same as

operator+=(z2,30.0);

cout << " Add 30 to sum:

"; showComplex(z2);

return 0;

}



Notice the use of the const keyword where appropriate, in showComplex(), in operator+(), and

in the first operator+=(). Notice the absence of the const keyword where appropriate: in the first

operator+=() and in the second operator+=(). Notice some advantages of object-oriented

programming in this example. Client code does not depend on the server design and the names of

the data fields (other than for initialization), and responsibility for low-level computations is pushed

down to the server functions. The meaning of high-level computations is expressed in function calls

to server functions. There are different areas of concern for low-level computations (handling fields

of complex numbers according to complex arithmetic) and high-level computations (handling

complex numbers according to whatever the application wants to achieve). There are separate areas

of change for data representation and for application algorithm: If the design of class Complex

changes, the overloaded operators change but not the client code; if the application algorithm

changes, the client code changes but not the overloaded operators.

Some advantages of object-oriented programming are absent: encapsulation is voluntary, there is

no indication that data and server functions belong together, and names of functions are global.

Does this sound familiar? Good. You are getting there.

The output of the run of this program is shown in Figure 10-2.



Figure 10-2. Output for program in Listing 10.2.



As you can see from Listing 10.2, it is all the same whether you use the spaces between the

keyword operator and the operator sign; operator+() and operator + () mean the same thing.

If the operator sign contains two symbols, they should be placed next to each other without an

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (547 of 1187) [8/17/2002 2:57:57 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



intervening space.

NOTE

The mandatory components of the name of an overloaded operator function are the keyword

"operator" and the symbol(s) for the operator. The keyword "operator" and the symbol(s) together

comprise the function name. You can insert white space between the keyword and the symbol(s) if

you feel that this enhances readability¡Xbreaking the function name into these two components is

not a syntax error.



Keep in mind that in all the cases of the use of overloaded operators in the client space, the

operation is implemented as a function call. You cannot use overloaded operators to speed up your

program. It is syntactic sugar that adds to readability of your program. In all the cases of the use of

overloaded operators the operator syntax can be replaced by the function call syntax. The last part

of the client code in Listing 10.2 could be written this way.

operator+=(z1,x);

// same as z1 += x;

cout << "Add first value to sum: " ; showComplex(z1);

operator+=(z2,30.0);

// same as z2 += 30.0;

cout << "Add 30 to sum:

" ; showComplex(z2);



Of course, you do not design overloaded operator functions just to use them with the function call

syntax in the client code. If you wanted to use a function call, you would call the function

addComplex(), not operator+(). You go to the trouble of defining overloaded operator functions

to use this special dispensation from the C++ compiler to treat the operator syntax as if it were a

function call. I keep reminding you about the function call syntax to make sure that you do not

forget that the operator syntax is compiled into a function call, not into arithmetic expression it

pretends to be.



Limitations on Operator Overloading

As you saw in the previous section, the overloaded operators give you a powerful tool to make C++

code more beautiful by treating objects of programmer-defined types similarly to variables of builtin numeric types. There are, however, some limitations that C++ places on the use of overloaded

operators. Some of these operations will not limit you much, but some are quite essential. In this

section, I will discuss these limitations.



What Operators Cannot Be Overloaded

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (548 of 1187) [8/17/2002 2:57:57 PM]



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Chapter 10. Operator Functions: Another Good idea

Tải bản đầy đủ ngay(0 tr)

×