Professional C__ - Marc Gregoire [130]
class Super
{
public:
void go() { cout << "go() called on Super" << endl; }
};
class Sub : public Super
{
public:
void go() { cout << "go() called on Sub" << endl; }
};
Attempting to call the go() method on a Sub object will initially appear to work.
Sub mySub;
mySub.go();
The output of this call is, as expected, go() called on Sub. However, since the method was not virtual, it was not actually overridden. Rather, the Sub class created a new method, also called go() that is completely unrelated to the Super class’s method called go(). To prove this, simply call the method in the context of a Super pointer or reference.
Sub mySub;
Super& ref = mySub;
ref.go();
You would expect the output to be, go() called on Sub, but in fact, the output will be, go() called on Super. This is because the ref variable is a Super reference and because the virtual keyword was omitted. When the go() method is called, it simply executes Super’s go() method. Because it is not virtual, there is no need to consider whether a subclass has overridden it.
Attempting to override a non-virtual method will “hide” the superclass definition and will only be used in the context of the subclass.
How virtual Is Implemented
To understand why method hiding occurs, you need to know a bit more about what the virtual keyword actually does. When a class is compiled in C++, a binary object is created that contains all of the data members and methods for the class. In the non-virtual case, the code to transfer control to the appropriate method is hard-coded directly where the method is called based on the compile-time type.
If the method is declared virtual, the correct implementation is called through the use of a special area of memory called the vtable, for “virtual table.” Each class that has one or more virtual methods has a vtable that contains pointers to the implementations of the virtual methods. In this way, when a method is called on an object, the pointer is followed into the vtable and the appropriate version of the method is executed based on the actual type of the object.
To better understand how vtables make overriding of methods possible, take the following Super and Sub classes as an example.
class Super
{
public:
virtual void func1() {}
virtual void func2() {}
};
class Sub : public Super
{
public:
virtual void func2() {}
};
For this example, assume that you have the following two instances:
Super mySuper;
Sub mySub;
Figure 8-11 shows a high-level view of how the vtables for both instances look. The mySuper object contains a pointer to its vtable. This vtable has two entries, one for func1() and one for func2(). Those entries point to the implementations of Super::func1() and Super::func2().
FIGURE 8-11
mySub also contains a pointer to its vtable which also has two entries, one for func1() and one for func2(). The func1() entry of the mySub vtable points to Super::func1() because Sub does not override func1(). On the other hand, the func2() entry of the mySub vtable points to Sub::func2().
The Justification for virtual
Given the fact that you are advised to make all methods virtual, you might be wondering why the virtual keyword even exists. Can’t the compiler automatically make all methods virtual? The answer is yes, it could. Many people think that the language should just make everything virtual. The Java language effectively does this.
The argument against making everything virtual, and the reason that the keyword was created in the first place, has to do with the overhead of the vtable. To call a virtual method, the program needs to perform an extra operation by dereferencing the pointer to the appropriate code to execute. This is a miniscule performance hit for most cases, but the designers of C++ thought that it was better, at least at the time, to let the programmer decide if the performance hit was necessary. If the method was never going to be overridden, there was no need to make it virtual and take the performance hit. However, with today’s CPUs, the performance hit is measured in fractions