Saturday, October 31, 2009

21.2 Virtual Functions




I l@ve RuBoard










21.2
Virtual Functions



Today there are many different ways of sending a letter. We can mail
it by the United States Postal Service, send it via Federal Express,
or even fax it. All these methods get the letter to the person to
whom you're sending it (most of the time), but they
differ in cost and speed.



Let's define a class called mail
to handle the sending of a letter. We start by defining an address
class and then use this class to define addresses for the sender and
the receiver. (The definition of the address class is
"just a simple matter of
programming" and is left to the reader.)



Our mail class looks like this:



class mail {
public:
address sender; // Who's sending the mail (return address)?
address receiver; // Who's getting the mail?

// Send the letter
void send_it( ) {
// ... Some magic happens here
};
};


There is, however, one little problem with this class:
we're depending on
"magic" to get our letters sent.
The process for sending a letter is different depending on which
service we are using. One way to handle this is to have
send_it call the appropriate routine depending on
what service we are using:



void mail::send_it(  ) {
switch (service) {
case POST_OFFICE:
put_in_local_mailbox( );
break;
case FEDERAL_EXPRESS:
fill_out_waybill( );
call_federal_for_pickup( );
break;
case UPS:
put_out_ups_yes_sign( );
give_package_to_driver( );
break;
//... and so on for every service in the universe


This solution is a bit clunky. Our mail class must know about all the
mailing services in the world. Also consider what happens when we add
another function to the class:



class mail {
public:
// Returns the cost of mailing in cents
int cost( ) {
// ... more magic
}


Do we create another big switch statement? If we
do, we'll have two of them to worry about.
What's worse, the sending instructions and cost for
each service are now spread out over two functions. It would be nice
if we could group all the functions for the Postal Service in one
class, all of Federal Express in another class, and so on.



For example, a class for the Postal Service might be:



class post_office: public mail{
public:
// Send the letter
void send_it( ) {
put_in_local_mailbox( );
};
// Cost returns cost of sending a letter in cents
int cost( ) {
// Costs 37 cents to mail a letter
return (37); // WARNING: This can easily become dated
}
};


Now we have the information for each single service in a single
class. The information is stored in a format that is easy to
understand. The problem is that it is not easy to use. For example,
let's write a routine to send a letter:



void get_address_and_send(mail& letter)
{
letter.from = my_address;
letter.to = get_to_address( );
letter.send_it( );
}
//...
class post_office simple_letter;
get_address_and_send(simple_letter);


The trouble is that letter is a
mail class, so when we call
letter.send_it( ), we call the
send_it of the base class mail.
What we need is a way of telling C++, "Please call
the send function of the derived class instead of
the base class."



The
virtual keyword identifies a member function
that can be overridden by a member function in the derived class. If
we are using a derived class, C++ will look for members in the
derived class and then in the base class, in that order. If we are
using a base class variable (even if the actual instance is a derived
class), C++ will search only the base class for the member function.
The exception is when the base class defines a virtual function. In this case, the derived
class is searched and then the base class.



Table 21-1 illustrates the various search
algorithms.


























Table 21-1. Member function search order

Class type



Member function type






Search order



Derived



Normal



Derived, then base



Base



Normal



Base



Base



Virtual



Derived, then base




Example 21-2 illustrates the use of virtual functions.




Example 21-2. virt/virt.cpp

// Illustrates the use of virtual functions
#include <iostream>

class base {
public:
void a( ) { std::cout << "base::a called\n"; }
virtual void b( ) { std::cout << "base::b called\n"; }
virtual void c( ) { std::cout << "base::c called\n"; }
};

class derived: public base {
public:
void a( ) { std::cout << "derived::a called\n"; }
void b( ) { std::cout << "derived::b called\n"; }
};

void do_base(base& a_base)
{
std::cout << "Call functions in the base class\n";

a_base.a( );
a_base.b( );
a_base.c( );
}

int main( )
{
derived a_derived;

std::cout << "Calling functions in the derived class\n";

a_derived.a( );
a_derived.b( );
a_derived.c( );

do_base(a_derived);
return (0);
}



The derived class contains three member functions. Two are
self-defined: a and b. The
third, c, is inherited from the base class. When
we call a, C++ looks at the derived class to see
whether that class defines the function. In this case it does, so the
line:



a_derived.a(  );


outputs:



derived::a called


When b is called the same thing happens, and we
get:



derived::b called


It doesn't matter whether the base class defines
a and b or not. C++ calls the
derived class and goes no further.



However, the derived class doesn't contain a member
function named c. So when we reach the line:



a_derived.c(  );


C++ tries to find c in the derived class and
fails. Then it tries to find the member function in the base class.
In this case it succeeds and we get:



base::c called


Now let's move on to the function
do_base. Because it takes a base class as its
argument, C++ restricts its search for member functions to the base
class. So the line:



a_base.a(  );


outputs:



base::a called


But what happens when the member function b is
called? This is a virtual function.
That tells C++ that the search rules are changed. C++ first checks
whether there is a b member function in the
derived class; then C++ checks the base class. In the case of
b, there is a b in the derived
class, so the line:



a_base.b(  );


outputs:



derived::b called


The member function c is also a virtual function. Therefore, C++ starts by
looking for the function in the derived class. In this case, the
function is not defined there, so C++ then looks in the base class.
The function is defined there, so we get:



base::c called


Now getting back to our mail. We need a simple base class that
describes the basic mailing functions for each different type of
service:



class mail {
public:
address sender; // Who is sending the mail (return address)?
address receiver; // Who is getting the mail?

// Send the letter
virtual void send_it( ) {
std::cout <<
"Error: send_it not defined in derived class.\n"
exit (8);
};
// Cost of sending a letter in pennies
virtual int cost( ) {
std::cout << "Error: cost not defined in derived class.\n"
exit (8);
};
};


We can define a derived class for each different type of service. For
example:



class post_office: public mail {
public:
void send_it( ) {
put_letter_in_box( );
}
int cost( ) {
return (29);
}
};


Now we can write a routine to send a letter and not have to worry
about the details. All we have to do is call
send_it and let the virtual function do the work.



The mail class is an abstraction that describes a
generalized mailer. To associate a real mailing service, we need to
use it as the base for a derived class. But what happens if the
programmer forgets to put the right member functions in the derived
class? For example:



class federal_express: public mail {
public:
void send_it( ) {
put_letter_in_box( );
}
// Something is missing
};


When we try to find the cost of sending a letter via Federal Express,
C++ will notice that there's no
cost function in
federal_express and call the one in
mail. The cost function in
mail knows that it should never be called, so it
spits out an error message and aborts the program. Getting an error
message is nice, but getting it at compilation rather than during the
run would be better.



C++ allows you to specify virtual
functions that must be overridden in
a derived class. For this example, the
new, improved, abstract mailer is:



class mail {
public:
address sender; // Who is sending the mail (return address)?
address receiver; // Who is getting the mail?

// Send the letter
virtual void send_it( ) = 0;
// Cost of sending a letter in pennies
virtual int cost( ) = 0;
};


The = 0 tells C++ that these member functions are

pure virtual
functions. That is, they can never be called
directly. Any class containing one or more pure virtual functions is
called an abstract class.
If you tried to use an abstract

class as an
ordinary type, such as:



void send_package(  ) {
mail a_mailer; // Attempt to use an abstract class


you would get a compile-time error.









    I l@ve RuBoard



    No comments:

    Post a Comment