21.2 Virtual Functions

I l @ ve RuBoard

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


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