Examining Virtual Member Functions: Are They for Real? - Inheritance - C++ For Dummies (2014)

C++ For Dummies (2014)

Part IV

Inheritance

Chapter 20

Examining Virtual Member Functions: Are They for Real?

In This Chapter

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

arrow Finding out how safe polymorphic nachos are

arrow Overriding member functions in a subclass

arrow 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
// overloaded at compile time vs. overriden at runtime
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class Student
{
public:
// uncomment one or the other of the next
// two lines; one binds calcTuition() early and
// the other late
// void calcTuition()
virtual 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(); // which calcTuition()?
}

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
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
return 0;
}

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.

image 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.

image 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.

image The type that you’ve been accustomed to until now is called the static, or compile-time, type. The compile-time 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 functionfn(), the run-time 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 run-time type is called polymorphism, or late binding. Deciding which overloaded function 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 OO if it doesn’t support polymorphism 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 world in which people live.

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 output of the OverloadOverride program the way it appears is as follows:

We're in Student::calcTuition
We're in Student::calcTuition
Press Enter to continue...

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.

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 run-time 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 Enter to continue...

image 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.

image 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 functionmakeACopy() 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)
{
Base* 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().

image This business of silently deciding when a function is overridden and when not is a source of error in C++; so much so that the 2011 standard introduced the descriptor override that the programmer can use to indicate her intent to override a base class function. C++ generates a compiler error if a function is declared override but does not, in fact, override a base class function for some reason (such as a mismatched argument) as in the following example:

class Student
{
public:
virtual void addCourseGrade(double grade);
};
class GradStudent : public Student
{
public:
virtual void addCourseGrade(float grade) override;
};

This snippet generates a compile-time error because the method GradStudent::addCourseGrade(float) was declared override but it does not, in fact, override the base class function Student::addCourseGrade(double) because the argument types don't match.

image The programmer can also declare a function as not overrideable using the final keyword, even if that function itself overrides some earlier base class function, as demonstrated in the following additional PostDoc class:

class GradStudent : public Student
{
public:
virtual void addCourseGrade(double grade) final;
};
class PostDoc : public GradStudent
{
public:
virtual void addCourseGrade(double grade);
};

Since Student::addCourseGrade() is marked final, the declaration of PostDoc::addCourseGrade() generates an error because it attempts to override the Student method.

image In addition, an entire class can be declared final:

class GradStudent final: public Student

This affects more than just the virtual methods of the class. A final class cannot be inherited from at all.

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 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.

image 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, then declare the class final (if your compiler supports this feature) and document it!