With all the attention given to casting, it's easy to forget that our ultimate goal is understanding polymorphism. Nevertheless, understanding the mechanics and consequences of casting will help us understand polymorphism and how to use it. So, let's review what we have learned about casting:
Casting is only possible between classes related by inheritance
Programs most often perform upcasts with pointers or references
Upcasting, converting a subclass to a superclass, can take place automatically, without an explicit casting operation
Downcasting, converting from a superclass to a subclass, requires an explicit cast operation
A program can cause an upcast with an assignment, but upcasts typically result from a function call
An upcast may make it impossible for a program to access subclass member variables, even though the object has them, without doing a downcast
Knowing the consequences of casting will help us understand why one is safe and the other is not and why programs must sometimes perform a downcast. There are two ways we can approach casting safety: mechanics and meaning.
Upcasting Is Safe But Downcasting Is Not
Let's first look at casting mechanics to help us understand why upcasting is safe while downcasting is not. To do this, we return to the problem introduced in the previous section of locating member variables stored in objects. But this time, we explore the consequences of downcasting in greater detail.
Person* ceo = new Employee(...);
Employee* cto = (Employee *)new Person(...);
(a)
(b)
(c)
Upcasting vs. downcasting. Why upcasting is safe but downcasting is not. Recall that inheritance is a unidirectional relationship: a subclass "knows about" its superclass, but a superclass "doesn't know about" its subclasses. So when the compiler looks up class Employee in its symbol table, it finds information about an Employee's two member variables, and, by following the inheritance link in the symbol table, it is also able to find information about Person and its three members. However, when the compiler looks up class Person, it only finds information about Person's members but cannot find information about Employee or its members.
The statement instantiates an Employee object (the large rectangle) that has five member variables - three that belong to Person (the blue part) and two that belong to Employee (the green part). The Employee object is upcast to a Person, which makes the id and phone members unreachable. Aside from losing access to the Employee members, the upcast causes no harm.
Symbol table entries for two classes, Person and Employee. Notice that the Employee entry has a pointer to the Person entry.
Alternatively, downcasting can create phantom members. The statement instantiates a Person (superclass) object but downcasts it to an Employee (subclass) object. The variable cto is a pointer to an Employee, which allows the compiler to access the Person members, which is completely safe. However, being an Employee pointer, it also allows the compiler to access the Employee members, which were never created because the program instantiated a Person object! Attempting to use the phantom members will produce unpredictable and completely incorrect results.
The potential problem with downcasting is that it may turn an object into something it is not. Changing an object into a different one allows the compiler to "locate" data that is not there. Memory is never blank or empty - it always contains some random bit pattern - but the contents are unrelated to an Employee object and don't have any meaning when accessed in this way. But C++ does provide syntax for downcasting, suggesting that it is sometimes needed, although it is potentially dangerous.
Downcasting To Reach Members
Polymorphism minimizes the need to perform a downcast, but it also provides two ways to test a potential downcast to see if it is safe or not. Unfortunately, in the absence of polymorphism, as shown in Figure 3, there is no elegant solution to the problem. Without polymorphism, we must know what kind of object we are dealing with before downcasting it. This limitation results in inelegant code, requiring a kludge based on an extra variable that "remembers" which class was originally instantiated (see Drawing the shape).
Slicing
Upcasting can work with an automatic or local variable, but polymorphism cannot. Furthermore, polymorphism requires a pointer or reference variable, so upcasting is typically done with pointers or references. Upcasts based on pointers or references do not alter the cast object in any way. But casts based on automatic or local variables copy the cast object, irrevocably changing the copy. Downcasting with pointers or references doesn't alter the object either. But what happens if we attempt an upcast with an automatic or local variable?
Employee e(...);
Person p;
p = e;
(a)
(b)
(c)
Upcasting with automatic or local variables results in slicing.
An instance of Employee is created - it consists of a Person sub-object (blue) and a part that is unique to Employee (green)
An "empty" Person object is created; this operation allocates only enough memory to hold a Person object
The Employee object is assigned (i.e., copied) to the Person object, but the Person object does not have space (i.e., memory) to hold the complete Employee object, and so the part of the Employee object that lies beyond the Person sub-object is sliced off