12.2.2. Safe Casting

Time: 00:06:39 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review
A UML class diagram of classes related by inheritance. The diagram only shows member variables.
Person
-----------------
-name : string
-height : double
-weight : int
-----------------

Employee : public Person
-----------------
-id : int
-phone : string
-----------------
Employee is a subclass of Person.

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:

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(...);
An abstract representation of an Employee object consisting of two parts. The first part is an instance of Person with three member variables. The second part is an instance of Person with two member variables. An abstract representation of the symbol table entries for two classes, Person and Employee. The Employee entry has three member variables and a pointer to the Person entry. The Person entry has two member variables. 'cto' points to a Person object, but its type is Employee, so the program should be able to reach the Employee's member variables. But there aren't any Employee members because the program instantiated a 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.
  1. 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.
  2. Symbol table entries for two classes, Person and Employee. Notice that the Employee entry has a pointer to the Person entry.
  3. 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

A UML class diagram. The Shape class has three subclasses: Circle, Rectangle, and Triangle.
A superclass with many subclasses. Knowing casting's meaning can help us understand why programs cast objects and the safety issues of doing so.

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?

An Employee object that is part Person and part Employee. An empty Person object. Upcasting by assigning (by value) an Employee object to a Person variable or object. The assignment operation copies the Employee to the Person, but the Person only has enough space to hold the Person part of the Employee, so the Employee part is sliced off.
Employee e(...);Person p;p = e;
(a)(b)(c)
Upcasting with automatic or local variables results in slicing.
  1. An instance of Employee is created - it consists of a Person sub-object (blue) and a part that is unique to Employee (green)
  2. An "empty" Person object is created; this operation allocates only enough memory to hold a Person object
  3. 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