Version 2: The dynamic_cast Operator
My next version removes the extra virtual methods of version 1 to reduce the memory requirements of the program while keeping the performance levels the same. Keep in mind that my version 1 needed an extra pointer for each and every object. In version 2, I simulate the desired polymorphism by using the C++ Runtime_type_information (RTTI) feature combined with the dynamic_cast operator within the parent's insertion function. The dynamic_cast operator converts a pointer or reference from one object type to another object type when both types are within the same class hierarchy (see C++ for Game Programmers, by Noel Llopis; Charles River Media, 2003). This is the only kind of cast operation in C++ that's checked at runtime. The other type conversions either don't check, or check at compile time. If the runtime conversion between pointer types is not valid, the dynamic_cast operation returns a null pointer (see The C++ Standard Library: A Tutorial and Reference, by Nicolia M. Josuttis; Addison-Wesley, 1999).
My strategy for version 2 is to use the dynamic_cast operator in a parent's function to find out if an object passed to this parent function is actually a child object. In other words, if I'm passed a child object, I process the parent's portion of the object, then I call the child's function to process the child's portion. This processing order mimics the first, polymorphic version of the code. Listing Two is the Employee example for version 2.
ostream& operator<<(ostream& os, const Employee& a Employee) { // code to print the Employee class if (dynamic_cast<const Manager*>(&a Employee) != NULL) os << *(dynamic_cast<const Manager*>(&a Employee)); return os; }
Getting back to my initial attempt to use the insertion operator (cout << *pEmp), the Employee class is the parent while the Manager is considered a child. In the Employee's insertion (<<) function, I always print the Employee's private variables. Afterwards, I use the dynamic_cast operator to see if I was passed a Manager object. If so, I then call the Manager's insertion function to handle printing the Manager's private variables. Thus, I've printed the Manager's portion using the Manager's function even though the Employee's function is called. This is how the simulated polymorphism is accomplished.
However, I found that using the dynamic_cast operator in this manner leads to a problem. The error shows up in a nonpolymorphic use of the insertion functions. For example:
Manager m1; cout << manager1;
Unlike the first example using the << function, this time, the Manager's insertion function is correctly called and passed a Manager object. When inside the Manager's function, I have to deal with the Employee portion of the object. This is typically done by calling the Employee's function from within the Manager's function:
ostream& operator<<(ostream& os, const Manager& aManager) { os << static_cast<const Employee&>(aManager); // code to print the Manager class return os; }
Unfortunately, this call to the Manager's function leads to an infinite recursive loop between the child and parent functions because they are now calling each other. To solve this problem, I introduced a Boolean flag. This flag is a static function variable because its value must be retained during the nested function calls. Listing Three is the updated version of the Manager's insertion function. It uses the Boolean flag I just discussed to stop the recursive loop problem.
ostream& operator<<(ostream& os, const Manager& aManager) { static bool Manager_printed = false; if (Manager_printed == false) { Manager_printed = true; os << static_cast<const Employee&>(aManager); Manager_printed = false; // code to print the Manager class } return os; }
Now, calling the Manager's function directly using the manager1 object from the second example correctly invokes the Employee's insertion function to print the Employee portion of manager1. Keep in mind that the Employee's insertion function then calls the Manager's function, but my new flag prevents the loop from going any further. The flag also prevents the Manager's portion of the object from being printed twice.
While this version correctly deals with being called directly by a Manager object, my original example using an Employee pointer that points to a Manager object has a problemthe Employee's function is called twice. The Employee's function is called first because it's processing an Employee pointer. Because the Employee's function was handed a Manager object, it correctly calls the Manager's insertion function. The problem arises within the Manager's function. The Manager's function should not call the Employee's function in this case. However, there is no way for it to tell that it was called by the Employee's function. To fix this problem, a Boolean flag is also added to the Employee function; see Listing Four.
ostream& operator<<(ostream& os, const Employee& a Employee) { static bool Employee_printed = false; if (Employee_printed == false) { // code to print the Employee class Employee_printed = true; if (dynamic_cast<const Manager*>(&a Employee) != NULL) os << *(dynamic_cast<const Manager*>(&a Employee)); Employee_printed = false; } return os; }
So, the Manager's insertion function successfully deals with its two possible usesbeing called directly by a Manager object, or being called indirectly by the Employee's insertion function. In the first case, it calls the Employee's insertion function to process the Employee's portion of the object. In the second case, the Employee's portion is being processed elsewhere.
Unfortunately, I found out that the executable size of the program grows when RTTI is turned on, so my desire to reduce memory requirements may not work as well as I hoped. To make matters even worse, for the dynamic_cast operation to work, the parent class must have at least one virtual method. Because one virtual method is required, the space for this version is the same as the previous versionone vtable for the class, plus a pointer to the vtable for every object. However, if the class already has a virtual method for some other reason, then additional space is not required for this version to implement polymorphism.