Tải bản đầy đủ - 0 (trang)
Chapter 15. Virtual Functions and other Advanced Uses of Inheritance

Chapter 15. Virtual Functions and other Advanced Uses of Inheritance

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

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



If the server object has only one client, so that the client object uses the server object exclusively,

the server object could be implemented as a data member of the client class even when the objects

are related with a general association and not aggregation.

The implementation of the client-server relationship with the server object as a data member of the

client object results in a middle degree of visibility. The server object is visible to all member

functions of the client class, but it is not visible to other classes in the program. The designers of

other classes do not have to learn the details of using this object and coordinate its use with other

designers.

A more limited degree of visibility is achieved when the server object is implemented as a local

variable in a member function of the client class. In this case, the server object is visible only to this

member function and to no other member function of the client class or any other class outside this

member function. A broader degree of visibility can be achieved when the server object is passed

as a parameter to a member function of the client class. In this case, the server object can be

associated with many other objects outside of the client class, and these objects have to cooperate

in using the server object.

Implementing associations by defining the server object as a local variable in a server method

results in fewer dependencies between parts of the program and hence in less complexity for the

implementers and the maintainers. Implementing associations by passing the server object as a

parameter to a client method results in greater flexibility but might increase the complexity of the

design for the implementers and the maintainers.

Choose whatever is most appropriate¡Xthe least degree of visibility that still supports client

requirements. The C++ programmer should always think about choosing one of these three

alternatives for implementing the association. The UML design notation does not distinguish

between these three techniques. Often, the designers do not even know which technique is most

appropriate in each case. They just state that the objects are related. So, it rests with the C++

programmer to make the right choice.

We also looked at the implementation of the specialization/generalization relationship between

classes. Using inheritance for implementing this relationship between classes allows the

programmer to build the server class in stages, implementing part of the server class functionality

in the base class and part in the derived class (or classes). Thus, inheritance is a powerful, flexible

mechanism for reusing C++ designs.

In this chapter, I will discuss advanced uses of inheritance programming with virtual functions and

abstract classes. In its simple form, the goal of utilizing inheritance is making the job of the server

class designer easier. In advanced uses, the goal of inheritance is making the job of the client

programmer easier by streamlining the client code. This is important when the client code deals

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



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



with collections of similar objects that undergo similar processing.

"Similar objects" means that they have attributes and operations in common, but some attributes

and operations are somewhat different between different kinds of objects. "Similar processing"

means that the client code treats these different kinds of objects basically the same way. However,

depending on the type of object, some things should be done somewhat differently.

For example, in the case study in the previous chapter, the inventory items of different kinds

(feature movies, comedy movies, or horror movies) were treated by the client code in the same

way. They were read from the file, linked to the customers that rented them, underwent the

checking in and checking out operations, and were saved to the file. In a few stages of processing

the items of different kinds were treated differently. For example, when the item data is displayed

on the screen, different labels should be displayed, depending on whether the item is a feature

movie, comedy movie, or horror movie.

This is why client code in the previous chapter had to use the switch statements to figure out what

kind of inventory item it was dealing with and what particular kind of processing should be used.

You will see that the use of virtual functions and abstract classes helps you streamline the client

code and eliminate this kind of run-time analysis from the client source code.

As the technical foundation for the use of virtual functions and abstract classes, I will first discuss

the issues related to using objects of one class where objects of another class are expected. The

C++ rules for this substitution with the use of inheritance are quite different from the rules for

nonrelated objects. They are also different from what our everyday intuition suggests about the

behavior of computational objects, and I will try to explain in what direction you should sharpen

your intuition.

And at the end of the chapter, I will discuss how the techniques of using inheritance, virtual

functions, and abstract classes can be extended to the case where a derived class has more than one

base class.

Programming with virtual functions and abstract classes is often presented as the essence of objectoriented programming. From the practical point of view this is not so. Most of C++ code deals with

cooperating objects and does not need virtual functions. Actually, most C++ code is (and should

be) written without the use of inheritance. However, programming with virtual functions is

definitely fun and is often useful. It is one of the most complex topics in C++, and I hope that you

will learn how to use virtual functions correctly and enjoy using them.



Conversions Between Nonrelated Classes

As stated earlier, C++ aspires to support the concept of strong typing. I would like to make sure

that you find this principle of modern programming intuitively natural and appealing: If the code

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



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



context expects an object of a particular type, it is a syntax error to use an object of a different type

instead.

What are possible contexts where this rule is important? They include:

ϒΠ



expressions and assignments



ϒΠ



function arguments (including pointers and references)



ϒΠ



objects used as targets of messages



I will call two different classes nonrelated if neither of them serves as a direct or indirect base class

for another class. Notice that the classes that are not related to each other through inheritance might

be associated with each other through aggregations and general associations. This is fine, but still

you cannot use objects of one class instead of objects of another class. If the classes are related

through inheritance, this is a different story.

Here is a small example that demonstrates all three contexts where C++ supports strong typing.

There are two classes, class Base and class Other, which are not related through inheritance. The

member function Base::set() expects an integer argument. The member function

Other::setOther() expects an argument of type Base, and the member function

Other::getOther() expects a pointer to a Base object. For simplicity, I do not include examples

with reference parameters, but everything I am going to say about pointers also holds for

references.

class Base {

int x;

public:

void set(int a)

{ x = a; }

int show() const

{ return x; }

} ;



// one class



class Other {

int z;

public:

void setOther(Base b)

{ z = b.show(); }

void getOther(Base *b) const

{ b->set(z); }

} ;



// another class



// modifier

// accessor



// modify target

// modify parameter



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



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



In the following client function main(), I define three objects to manipulate, one each of type Base

and type Other, and one of a numeric type. The second line is correct and trivial: The parameter is

of the correct type, and the message target is of the correct type. The third line is also correct and

trivial: The expression operands are compatible with each other, and the assignment target is

compatible with the type of the rvalue.

Compatibility here means that the values of two different types (in this case, integer and double

floating point) have the same operations defined for them (addition, assignment), and the values

can be converted from one type to another (integer to double) and back (double to integer).

The next two statements are also correct. The message names in these statements (setOther() and

getOther()) match the member function names described in the target class (class Other), and the

message arguments are of the correct type (class Base on the fourth line or class Base pointer on

the fifth line). All other statements in the client code are incorrect, and I commented them out. Let

us discuss each statement and its problems in turn.

int main()

{

Other a; Base b;

b.set(10);

x = 5 + 7.0;

a.setOther(b);

a.getOther(&b);

// b = 5 + 7;

// x = b + 7;

// b.set(a);

// a.setOther(5);

// a.getOther(&a);

// b.getOther(&b);

// x.getOther(&b);

return 0; }



int x;



//

//

//

//

//

//

//

//

//

//

//

//



create objects

OK: correct parameter and target types

OK: right types for expression and lvalue

OK: right type for the target, argument

OK: right type for the target, argument

not OK: no operator = (int) defined

not OK: no operator or conversion to int

not OK: an object as a numeric argument

not OK: cannot convert number to object

no: no conversion from Other* to Base*

not OK: wrong target type, not a member

not OK: a number as a message target



In the first assignment (see below), the compiler expects an lvalue of a numeric type; instead, I use

an object of a programmer-defined type. The compiler would like me to define the assignment

operator=(int) with an integer argument for the type Base. This would make the first statement

legal. In the second case, I add an object of the programmer-defined type and a numeric variable.

These types are incompatible. For this statement to be legal, the compiler would like me to define

the operator+(int) for the type Base. In both cases, the C++ attitude is noncompromising:

Strong typing weeds out errors at the compilation stage rather than at run time.

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



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



b = 5 + 7;



x = b + 7;



// syntax errors



The next two statements deal with parameter passing. If a function, for example, Base::set(int),

expects an argument of a numeric type, you cannot use an object of a programmer-defined class

instead. A conversion operator could help, but we will study it only in the next chapter. Conversely,

if a function, for example, Other::setOther(Base), expects an argument of a particular

programmer-defined type, you cannot use a numeric value or a value of some other programmerdefined type instead. In all these cases, the compiler refuses to convert the value of one type to the

value of another type and labels them as compile-time errors.

b.set(a);



a.setOther(5);



// syntax errors



C++ also tries to support the principle of strong typing for pointers and references. I lumped

together pointers and references because the rules for them are the same. If a function has a

parameter that is defined as a pointer (or a reference) to an object of some type it is an error to pass

to it a pointer (or a reference) to an object of any other type, built-in or programmer-defined.

a.getOther(&a);



// syntax error



It goes without saying that a function that expects a pointer cannot be called with a reference or an

object as an actual argument, even if the reference or the object is of the same type as the pointer.

Similarly, if a function expects a reference parameter, the actual argument cannot be a pointer, even

if it is of the same type (it is fine to pass an object as the actual argument to a function with a

reference parameter).

For message targets, the concept of strong typing manifests itself in limiting the set of messages

that could be legitimately sent to a given target (an object, pointer, or reference). If the name of the

message sent to an object is not found in the class specification, it is an error regardless of whether

this function is found in any other class or in no class at all. For the compiler, it is enough that the

message is not found in the class to which the message target belongs. And, of course, you cannot

send a message to a numeric variable or value because a numeric variable or value does not belong

to any class and can respond to no messages. The compiler needs a variable of a programmerdefined type to the left of the dot selector operator.



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



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



b.setOther(b);



x.setOther(b) ;



// incorrect target types



A pointer (and a reference) variable can point to a value of only one type, the type used in its

declaration. This is yet another manifestation of strong typing. In this next code snippet, the second

line is correct, but the third line is not.

Other a; Base b;

Base &r1 = b; Base *p1 = &b;

Base &r2 = a; Base *p2 = &a;



// OK: compatible types

// non-compatible types



Strong Typing and Weak Typing

This is the ideal state of affairs. However, C++ allows a number of exceptions to these strict rules.

Some of these liberties C++ inherits from C.

For example, all numeric types are considered equivalent from the point of view of type checking.

You can mix them freely in an expression, and the compiler will silently convert "smaller"

operands to "larger" operands so that all operators are applied to the operands of the same type. On

the right-hand side of an assignment or as an argument in a function call, you can use a value of a

"larger" numeric type where a value of a "smaller" numeric type is expected. The compiler will

again silently convert the "larger" value (e.g., a long integer) into the "smaller" value (e.g., a

character). The compiler assumes, so to speak, that you know what you are doing.

If you use a numeric value of a "larger" type where a value of a "smaller" type is expected, some

compilers might give you a warning message. This happens, for example, when you try to squeeze

a double floating-point value into an integer or into a character variable. But this is just a warning,

not a syntax error. Following C, C++ allows you to use explicit casts to indicate to the reader the

intent of the designer to convert a value of one numeric type into a value of another numeric type.

But this is just an option for maintenance-conscious programmers. For brevity-starving

programmers, again following C, C++ makes all implicit conversions between numeric types legal.

This liberal attitude toward a potential loss of precision applies both to assignments and to

parameter passing.

From this point of view, C++ is (similar to C) a weakly typed language. In all these cases, the

compiler assumes that you know what you are doing and does not try to second-guess you. If you

do not know what you are doing or you do not pay attention to this side of your computation, well,

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



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



let us hope that your computation indeed does not depend on the precision of the truncated values.

C++ also supports other exceptions to the rules of strong typing that cannot be blamed on the

backward compatibility with C. These exceptions stem from the use of special member functions

that C++ allows you to add to the design of your classes and from the use of casts:

ϒΠ



conversion constructors



ϒΠ



conversion operators



ϒΠ



casts between pointers (or references)



These special functions represent the ways to talk the C++ compiler into accepting the client code

that violates the rules of strong typing.



Conversion Constructors

Assume, for example, that class Base provides a conversion constructor with a numeric argument.

Base::Base(int xInit = 0)

{ x = xInit; }



// conversion constructor



With this constructor available, this statement now compiles.

a.setOther(5);



// incorrect type, but no syntax error



The compiler interprets this message in the following way.

a.setOther(Base(5));



// compiler's point of view



A temporary object of class Base is created, initialized with a call to the conversion constructor,

used as an actual argument of the correct type, and then destroyed. Hence, the requirement of

strong typing is satisfied at the compiler level¡Xthe function gets a value of the type it needs. This

requirement is not satisfied at the programmer level; the programmer passes to setOther() an

argument of an incorrect type and gets away with it.

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



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



Notice that I supplied the default parameter value to this constructor. Why did I do that? Before I

added this constructor to class Base, it had the system-supplied default constructor, and I was able

to define class Base objects without arguments. With the conversion constructor in place, the

system took away the default constructor, and the definitions of Base objects without arguments

would become syntactic errors.

As I mentioned earlier, C++ has this exceptional ability to make existing code syntactically

incorrect when a new segment of code (in this case, the constructor) is added without removing

anything from the code. In other languages, when you add new code, you can make non-related

parts of the program run incorrectly, but you cannot make it syntactically incorrect. From one point

of view, this is distressing, because adding nonrelated code should not cause problems in existing

parts of the program. From another point of view, this is exciting, because the compiler notifies the

programmer about problems at compile time, not at run time.

To avoid these problems, I could have added to class Base a programmer-defined default

constructor that does nothing. This was probably the best solution because I did not need the object

to be initialized to any particular value. (I do not have any further use for that value.) But I was lax,

and instead of adding yet another constructor, I just added the zero default parameter value to make

the existing client code compile. What is the drawback of this solution? I pretend that this zero

value is somehow used elsewhere. Meanwhile, it is not used. I know that it is not used, but the

maintainer will have to figure that out. Hence, I made a contribution (however small) to increasing

the level of difficulty of reading this code.

So, adding the conversion constructor to class Base makes the call to the member function

Other::setOther(Base) compile with the actual argument of a numeric type.

a.setOther(5);



// the same as a.setOther(Base(5));



When the compiler does not find the exact match for the parameter type, it will search for a

possible numeric conversion. If there is no appropriate numeric conversion, it will search for a

combination of a numeric and programmer-defined conversion. The conversion constructor is one

of the possible programmer-defined conversions.

With this constructor in place, the next statement also becomes legitimate because the compiler

calls the conversion constructor to satisfy the requirements of strong typing.

b = 5 + 7;



// no error: the same as b = Base(5+7);



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



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



In a sense, the compiler is trying to second-guess the programmer. One of the goals of C++ design

was to avoid this and let the programmer explicitly state what the code means. One of the ways to

explicitly state what we mean is to use explicit casts. However, according to C/C++ rules of weak

typing, explicit casts are not required for conversions among numeric types, and the calls to the

conversion constructors can be done implicitly, without explicit calls. What to do? The ISO/ANSI

standard comes with a compromise. If the class designer feels that the conversion constructor

should be called only explicitly, the keyword explicit is used as the modifier. (See Chapter 10,

"Operator Functions: Another Good Idea," for more examples.)

explicit Base::Base(int xInit = 0)

{ x = xInit; }



// no implicit calls



The use of the keyword explicit is optional. If you use it in the design of the Base class, your

code will be harder to write. (You use this extra keyword explicit.) Also, the client code will be

harder to write (the client programmer will use explicit casts), but the resulting code will be easier

to understand. If you do not use this keyword, you will make everybody's life (including yours)

easier, but the quality of the code will suffer. It is hard to strike a good compromise.

C++ allows all kinds of silent conversions, but the explicit keyword prevents you from using them.

This statement now is a syntax error again despite the presence of the conversion constructor; it

needs an explicit cast.

a.getOther(5);



// illegal if constructor is defined as explicit



Notice that the implicit conversions apply only to an argument that is passed by value. It does not

work with reference and pointer parameters. Adding the conversion constructor would not make a

call to Other::getOther(Base* b) compile with the numeric argument.

int x = 5

a.getOther(&x);



// is this is still a syntax error



Casts Between Pointers (or References)

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



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



The rules for implicit conversions (weak typing for values) apply only to values and not to

references or to pointers (strong typing for addresses). However, explicit conversions can be

applied to arguments of any nature. Can you pass an integer pointer to the place where the Base

pointer is expected? No; according to the rules of strong typing, this line is an error:

a.getOther(&x);



// syntax error



However, you can always tell the compiler that you know what you are doing and you want it to

accept this code. The C++ way of telling the compiler that you know what you are doing is to use

an explicit cast to the correct type.

a.getOther((Base*)&x);



// no problem, conversion is OK!!



In this function call, a Base pointer is created and is initialized to point to the memory location that

contains x. Inside getOther(), Base class messages are sent to the area occupied by x. Since

Base methods do not know about the data structure of x, they can easily damage it. The whole

operation does not make sense at all, but it is legal in C++. If you insist that you know what you are

doing, the compiler will not argue with you.

The same is true about pointer (or reference) conversions among pointers (or references) of any

type. Implicit conversions among different types are not allowed. For example, this is an error.

a.getOther(&a);



// error: no conversion from Other* to Base*



The method getOther() expects a pointer of type Base. Instead, it gets a pointer to an object of

type Other. According to the principles of strong typing, the compiler flags this line as a syntax

error¡Ximplicit casts between pointers (or references) of different types are not allowed. However,

the function call with an explicit case is acceptable to the compiler.

a.getOther((Base*)&a);



// no problem, explicit conversion is OK



Here, a pointer to the Base class object is created and initialized to point to the Other object a.

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



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



This pointer is passed to the getOther() method as an actual argument. Inside the getOther()

method, this pointer is used to send to the Other object messages that belong to class Base. The

compiler cannot flag these messages as erroneous. The execution of the program might result in a

crash or might quietly produce incorrect results. This code is utter nonsense, but it is legal C++.



Conversion Operators

As far as the conversion operators are concerned, they are used as regular C++ casts. When applied

to objects of programmer-defined types, they usually return a value of one of the object

components. For example, the cast to int applied to an object of type Other might return the value

of the data member x. The use of this operator eliminates a syntax error when an object of type

Other is used where an integer (or other numeric type) is expected.

b.set(a);



// the same as b.set(int(a));



Of course, this does not become legal just because I want it to. This is an example of client code

that should be supported by adding appropriate services to the server class Other. I will tell you

how to implement this kind of service in Chapter 16, "Advanced Uses of Operating Overloading."

But the moral of using conversion operators is clear¡Xthis is yet another blow to the C++ system of

strong typing.

If you wrote this code because you wanted the conversion from Other to int to happen, fine (using

explicit cast would be better). If you used the object a instead of an integer by mistake, the

compiler does not stand by to tell you about it. The protection of strong typing is removed, and you

have to discover the error through run-time testing and debugging.

In summary, C++ is a weakly typed language as far as numeric types are concerned. You can

convert from one numeric type to another freely, and explicit casts are not required. If you made a

mistake, beware.

C++ is a strongly typed language as far as programmer-defined types are concerned. The language

provides no casts between numeric types and programmer-defined types or between different

programmer-defined types. If you made a mistake, it is flagged as a syntax error, and you can

correct it before running the program.

Conversion constructors and conversion operators weaken the C++ system of strong typing for

programmer-defined types. They allow explicit and even implicit conversions between numeric

types and programmer-defined types. If you make a mistake, beware: It is not flagged as a syntax

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



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

Chapter 15. Virtual Functions and other Advanced Uses of Inheritance

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

×