Professional C__ - Marc Gregoire [131]
There is also a tiny hit to code size. In addition to the implementation of the method, each object would also need a pointer, which takes up a tiny amount of space.
The Need for virtual Destructors
Even programmers who don’t adopt the guideline of making all methods virtual still adhere to the rule when it comes to destructors. The reason is that making your destructors non-virtual can easily result in situations in which memory is not freed by object destruction.
For example, if a subclass uses memory that is dynamically allocated in the constructor and deleted in the destructor, it will never be freed if the destructor is never called. As the following code shows, it is easy to “trick” the compiler into skipping the call to the destructor if it is non-virtual:
class Super
{
public:
Super();
~Super();
};
class Sub : public Super
{
public:
Sub() { mString = new char[30]; }
~Sub() { delete [] mString; }
protected:
char* mString;
};
int main()
{
Super* ptr = new Sub(); // mString is allocated here.
delete ptr; // ~Super is called, but not ~Sub because the destructor
// is not virtual!
return 0;
}
Unless you have a specific reason not to, we highly recommend making all methods, including destructors but not constructors, virtual. Constructors cannot and need not be virtual because you always specify the exact class being constructed when creating an object.
Run Time Type Facilities
Relative to other object-oriented languages, C++ is very compile-time oriented. Overriding methods, as you learned above, works because of a level of indirection between a method and its implementation, not because the object has built-in knowledge of its own class.
There are, however, features in C++ that provide a run time view of an object. These features are commonly grouped together under a feature set called Run Time Type Information, or RTTI. RTTI provides a number of useful features for working with information about an object’s class membership. One such feature is dynamic_cast to safely convert between types within an OO hierarchy and is discussed earlier in this chapter.
A second RTTI feature is the typeid operator, which lets you query an object at run time to find out its type. For the most part, you shouldn’t ever need to use typeid because any code that is conditionally run based on the type of the object would be better handled with virtual methods.
The following code uses typeid to print a message based on the type of the object:
#include void speak(const Animal& inAnimal) { if (typeid(inAnimal) == typeid(Dog&)) { cout << "Woof!" << endl; } else if (typeid(inAnimal) == typeid(Bird&)) { cout << "Chirp!" << endl; } } Anytime you see code like this, you should immediately consider reimplementing the functionality as a virtual method. In this case, a better implementation would be to declare a virtual method called speak() in the Animal class. Dog would override the method to print "Woof!" and Bird would override the method to print "Chirp!". This approach better fits object-oriented programming, where functionality related to objects is given to those objects. In a polymorphic situation, the typeid operator will work correctly only if the classes have at least one virtual method. One of the primary values of the typeid operator is for logging and debugging purposes. The following code makes use of typeid for logging. The logObject function takes a “loggable” object as a parameter. The design is such that any object that can be logged subclasses the Loggable class and supports a method called getLogMessage(). In this way, Loggable is a mix-in class. #include void logObject(Loggable& inLoggableObject) { logfile << typeid(inLoggableObject).name() << " "; logfile << inLoggableObject.getLogMessage()