Version 3: An Enumerated Type
My final version replaces the dynamic_cast operator with a type identifier added to the parent class. No virtual method is needed for any of the classes. This lets me reduce the memory required for each object from a 32-bit pointer pointing to the vtable to an 8-bit character that stores the type identifier. In addition, no memory for the class's vtable is needed since a virtual method is not used nor is it required for the dynamic_cast operator because this operator is also not being used. The type identifier is an enumerated type stored in a character variable that is added to the Employee class; see Listing Five.
enum type { EMPLOYEE=1, MANAGER=2, SALESPERSON=3 }; class Employee { public: char _type; Employee() { _type= EMPLOYEE; } friend ostream& operator<<(ostream& os, const Employee& aEmployee); }; class Manager : public Employee { public: Manager() { _type = MANAGER; } friend ostream& operator<<(ostream& os, const Manager& aManager); };
Objects must initialize the type identifier in their constructors. For brevity, I made the type identifier public but it could also be made private. Protected accessor and mutator methods would also be supplied for this option.
The new type identifier serves two purposesit identifies the type of object passed to the parent's insertion function, and it replaces the Boolean flag used in the previous version. The flag stops the infinite recursion between the two insertion functions. The type identifier can act as a flag if it is altered while the two functions are executing, but reset prior to exiting the insertion operation.
Listing Six is version 3 of the example. The code for the Employee insertion function extends the previous examples so that it works for a situation where the Employee class has two childrena Manager and a Salesperson class. The Employee insertion function processes the Employee portion of the class before checking the type identifier. If a Manager object was passed into the Employee function, the type identifier is changed, then the Manager's function is called.
ostream& operator<<(ostream& os, const Employee& aEmployee) { // code to print the Employee class if (aEmployee._type == MANAGER) { const_cast<Employee&>(aEmployee)._type = - aEmployee._type; os << static_cast<const Manager&>(aEmployee); const_cast<Employee&>(aEmployee)._type = - aEmployee._type; } else if (aEmployee._type == SALESPERSON) { const_cast<Employee&>(aEmployee)._type = - aEmployee._type; os << static_cast<const Manager&>(aEmployee); const_cast<Employee&>(aEmployee)._type = - aEmployee._type; } return os; } ostream& operator<<(ostream& os, const Manager& aManager) { if (aManager._type == MANAGER) { const_cast<Manager&>(aManager)._type = - aManager._type; os << static_cast<const Employee&>(aManager); const_cast<Manager&>(aManager)._type = - aManager._type; } // code to print the Manager class return os; }
The Manager's insertion function checks the object's type identifier before calling the Employee's insertion function. If the type identifier has been changed, then we know that the Manager's function was called by its parent and, thus, should not be called again. If the type identifier has not been modified, then we know it's the nonpolymorphic case, and therefore, we will have to call the Employee's insertion function to process the Employee portion of the object.
While this behavior appears equivalent to version 2, the behavior is actually slightly improved. Version 2 calls the Employee's function twice for the first example (cout << *pEmp). However, version 3 does not call the Employee function a second time because the Manager's function sees that the Employee's function changed the type identifier. Replacing version 2's static function variable with a class variable allows for this improvement.