Chapter 20. Examining Virtual Member Functions: Are They for Real?

In This Chapter

  • Discovering how polymorphism (a.k.a. late binding) works

  • Finding out how safe polymorphic nachos are

  • Overriding member functions in a subclass

  • Checking out special considerations with polymorphism

The number and type of a function's arguments are included in its full, or extended, name. This enables you to give two functions the same name as long as the extended name is different:

void someFn(int);
void someFn(char*);
void someFn(char*, double);

In all three cases, the short name for these functions is someFn() (hey! this is some fun). The extended names for all three differ: someFn(int) versus someFn(char*), and so on. C++ is left to figure out which function is meant by the arguments during the call.

Member functions can be overloaded. The number of arguments, the type of arguments, and the class name are all part of the extended name.

Inheritance introduces a whole new wrinkle, however. What if a function in a base class has the same name as a function in the subclass? Consider, for example, the following simple code snippet:

class Student
{
  public:
    double calcTuition();
};
class GraduateStudent : public Student
{
  public:
    double calcTuition();
};

int main(int argcs, char* pArgs[])
{
   Student s;
   GraduateStudent gs;
   s.calcTuition(); //calls Student::calcTuition()
   gs.calcTuition();//calls GraduateStudent::calcTuition()
   return 0;
}

As with any overloading situation, when the programmer refers to calcTuition(), C++ has to decide which calcTuition() is intended. Obviously, if the two functions differed in the type of arguments, there's no problem. Even if the arguments were the same, the class name should be sufficient to resolve the call, and this example is no different. The call s.calcTuition() refers to Student::calcTuition() because s is declared locally as a Student, whereas gs.calcTuition() refers to GraduateStudent::calcTuition().

But what if the exact class of the object can't be determined at compile time? To demonstrate how this can occur, change the preceding program in a seemingly trivial way:

//
//  OverloadOverride - demonstrate when a function is
//                     declare-time overloaded vs. runtime
//                     overridden
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
  public:
    void calcTuition()
    {
        cout << "We're in Student::calcTuition" << endl;
    }
};
class GraduateStudent : public Student
{
  public:
    void calcTuition()
    {
        cout << "We're in GraduateStudent::calcTuition"
             << endl;
    }
};

void fn(Student& x)
{
    x.calcTuition(); // to which calcTuition() does
                     // this refer?
}

int main(int nNumberofArgs, char* pszArgs[])
{
    // pass a base class object to function
    // (to match the declaration)
    Student s;
    fn(s);

    // pass a specialization of the base class instead
    GraduateStudent gs;
    fn(gs);

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    system("PAUSE");
    return 0;
}

This program generates the following output:

We're in Student::calcTuition
We're in Student::calcTuition
Press any key to continue ...

Instead of calling calcTuition() directly, the call is now made through an intermediate function, fn(). Depending on how fn() is called, x can be a Student or a GraduateStudent. A GraduateStudent IS_A Student.

Note

Refer to Chapter 19 if you don't remember why a GraduateStudent IS_A Student.

The argument x passed to fn() is declared to be a reference to Student.

Note

Passing an object by reference can be a lot more efficient than passing it by value. See Chapter 17 for a treatise on making copies of objects.

You might want x.calcTuition() to call Student::calcTuition() when x is a Student but to call GraduateStudent::calcTuition() when x is a GraduateStudent. It would be really cool if C++ were that smart.

Tip

The type that you've been accustomed to until now is called the static, or compile-time, type. The declared type of x is Student in both cases because that's what the declaration in fn() says. The other kind is the dynamic, or runtime, type. In the case of the example function fn(), the runtime type of x is Student when fn() is called with s and GraduateStudent when fn() is called with gs. Aren't we having fun?

The capability of deciding at runtime which of several overloaded member functions to call based on the runtime type is called polymorphism, or late binding. Deciding which overloaded to call at compile time is called early binding because that sounds like the opposite of late binding.

Overloading a base class function polymorphically is called overriding the base class function. This new name is used to differentiate this more complicated case from the normal overload case.

Why You Need Polymorphism

Polymorphism is key to the power of object-oriented programming. It's so important that languages that don't support polymorphism can't advertise themselves as OO languages. (I think it's a government regulation — you can't label a language that doesn't support OO unless you add a disclaimer from the Surgeon General, or something like that.)

Without polymorphism, inheritance has little meaning. Remember how I made nachos in the oven? In this sense, I was acting as the late binder. The recipe read: Heat the nachos in the oven. It didn't read: If the type of oven is microwave, do this; if the type of oven is conventional, do that; if the type of oven is convection, do this other thing. The recipe (the code) relied on me (the late binder) to decide what the action (member function) heat means when applied to the oven (the particular instance of class Oven) or any of its variations (subclasses), such as a microwave oven (Microwave). This is the way people think, and designing a language along the lines of the way people think allows the programming model to more accurately describe the real world.

How Polymorphism Works

Any given language can support either early or late binding based upon the whims of its developers. Older languages like C tend to support early binding alone. Recent languages like Java and C# support only late binding. As a fence straddler, C++ supports both early and late binding.

You may be surprised that the default for C++ is early binding. The reason is simple, if a little dated. First, C++ has to act as much like C as possible by default to retain upward compatibility with its predecessor. Second, polymorphism adds a small amount of overhead to every function call both in terms of data storage and code needed to perform the call. The founders of C++ were concerned that any additional overhead would be used as a reason not to adopt C++ as the system's language of choice, so they made the more efficient early binding the default.

One final reason is that it can be useful as a programmer of a given class to decide whether you want a given member function to be overridden at some time in the future. This argument is strong enough that Microsoft's new C# language also allows the programmer to flag a function as not overridable (however, the default is overridable).

To make a member function polymorphic, the programmer must flag the function with the C++ keyword virtual, as shown in the following modification to the declaration in the OverloadOveride program:

class Student
{
  public:
    virtual void calcTuition()
    {
        cout << "We're in Student::calcTuition" << endl;
    }
};

The keyword virtual that tells C++ that calcTuition() is a polymorphic member function. That is to say, declaring calcTuition() virtual means that calls to it will be bound late if there is any doubt as to the runtime type of the object with which calcTuition() is called.

Executing the OverloadOveride program with calcTuition() declared virtual generates the following output:

We're in Student::calcTuition
We're in GraduateStudent::calcTuition
Press any key to continue ...

Note

If you're comfortable with the debugger that comes with your C++ environment, you really should single-step through this example. It's so cool to see the program single-step into Student::calcTuition() the first time that fn() is called but into GraduateStudent::calcTuition() on the second call. I don't think that you can truly appreciate polymorphism until you've tried it.

Tip

You need to declare the function virtual only in the base class. The "virtualness" is carried down to the subclass automatically. In this book, however, I follow the coding standard of declaring the function virtual everywhere (virtually).

When Is a Virtual Function Not?

Just because you think that a particular function call is bound late doesn't mean that it is. If not declared with the same arguments in the subclasses, the member functions are not overridden polymorphically, whether or not they are declared virtual.

One exception to the identical declaration rule is that if the member function in the base class returns a pointer or reference to a base class object, an overridden member function in a subclass may return a pointer or reference to an object of the subclass. In other words, the function makeACopy() is polymorphic, even though the return type of the two functions differ:

class Base
{
  public:
    // return a copy of the current object
    Base* makeACopy();
};

class SubClass : public Base
{
  public:
    // return a copy of the current object
    SubClass* makeACopy();
};

void fn(Base& bc)
{
    BaseClass* pCopy = bc.makeACopy();

    // proceed on...
}

In practice, this is quite natural. A makeACopy() function should return an object of type SubClass, even though it might override BaseClass::makeACopy().

Considering Virtual Considerations

You need to keep in mind a few things when using virtual functions. First, static member functions cannot be declared virtual. Because static member functions are not called with an object, there is no runtime object upon which to base a binding decision.

Second, specifying the class name in the call forces a call to bind early, whether or not the function is virtual. For example, the following call is to Base::fn() because that's what the programmer indicated, even if fn() is declared virtual:

void test(Base& b)
{
  b.Base::fn();     // this call is not bound late
}

Finally, constructors cannot be virtual because there is no (completed) object to use to determine the type. At the time the constructor is called, the memory that the object occupies is just an amorphous mass. It's only after the constructor has finished that the object is a member of the class in good standing.

By comparison, the destructor should almost always be declared virtual. If not, you run the risk of improperly destructing the object, as in the following circumstance:

class Base
{
  public:
    ~Base();
};

class SubClass : public Base
{
  public:
    ~SubClass();
};

void finishWithObject(Base* pHeapObject)
{
    // ...work with object...
    // now return it to the heap
    delete pHeapObject; // this calls ~Base() no matter
}                       // the runtime type of
                        // pHeapObject

If the pointer passed to finishWithObject() really points to a SubClass, the SubClass destructor is not invoked properly — because the destructor has been not been declared virtual, it's always bound early. Declaring the destructor virtual solves the problem.

So when would you not want to declare the destructor virtual? There's only one case. Virtual functions introduce a "little" overhead. Let me be more specific. When the programmer defines the first virtual function in a class, C++ adds an additional, hidden pointer — not one pointer per virtual function, just one pointer if the class has any virtual functions. A class that has no virtual functions (and does not inherit any virtual functions from base classes) does not have this pointer.

Now, one pointer doesn't sound like much, and it isn't unless the following two conditions are true:

  • The class doesn't have many data members (so that one pointer represents a lot compared to what's there already).

  • You intend to create a lot of objects of this class (otherwise, the overhead doesn't make any difference).

If these two conditions are met and your class doesn't already have virtual member functions, you may not want to declare the destructor virtual.

Warning

Except for this one case, always declare destructors to be virtual, even if a class is not subclassed (yet) — you never know when someone will come along and use your class as the base class for her own. If you don't declare the destructor virtual, document it!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.98.208