12.2. Casting Objects

Time: 00:06:15 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review
I first saw the term "upcast" in Thinking in C++, by Bruce Eckel, 1995 (Prentice Hall). Although the term does not appear in every C++ text, I continue to use and expand on it because it succinctly and accurately describes a critical concept.

Converting a value from one data type to another is called typecasting or casting for short. Casting an object from one class type to another makes sense when the original classy type is a subclass of the destination type. For example, if Circle is a subclass of Shape (see Figure 4 below), a Circle "is a" Shape, so it makes sense to think of a Circle as a Shape or to convert an instance of Circle to an instance of Shape. However, it doesn't make sense to convert an Employee to Shape. Although we didn't call it "upcasting" at the time, we've used this operation previously.

friend ostream& operator<<(ostream& out, Actor& me)
{
	out << (Person &)me << " " << me.agent << endl;
	return out;
}
Upcasting in the inserter operator. Previously, we created a chain of inserter function calls by having a subclass inserter call its superclass inserter. Building the chain required casting a subclass object to a superclass to match the superclass function's parameters. The casting operation doesn't change the cast object. Instead, it forms an expression whose result is the superclass type. Typically, we cast objects using references, as illustrated here, or pointers. Casting objects by value results in slicing, which we discuss later in this chapter.

Upcasting

Upcasting, as its name suggests, combines two concepts. First, a casting operation converts a value represented by one data type to an appropriate value represented by a different type. Second, when we cast objects, we always do so in the context of inheritance. If we arrange the inheritance hierarchy with each superclass above its subclasses, casting from a subclass to its superclass always takes place upwards in an inheritance hierarchy. (However, you should keep in mind that as UML class diagrams become large and complex, it's not always possible to place superclasses above their subclasses; we use "upwards" to mean "in the direction of the inheritance arrows.")

A tall, narrow inheritance hierarchy with class A is at the top. B is a subclass of A; C is a subclass of B. Class D is a subclass of C and is at the bottom.
  • 1. D → A
  • 2. D → B
  • 3. D → C
  • 4. C → A
  • 5. C → B
  • 6. B → A
(a)(b)
Upcasting options. Programmers may upcast an object to any convenient superclass.
  1. An inheritance hierarchy with four classes arranged in four levels. Class A is a superclass to classes B, C, and D. Classes B and C are both a superclass and a subclass. Class D is only a subclass.
  2. Upcasting can occur anywhere along an inheritance hierarchy, beginning and ending at any level. Six different upcasts are possible in this example.
D my_d;
A* a_ptr = &D;
A* a_ptr = new D;
 
(a)(b)
Upcasting with assignment. Programmers perform a simple upcast with an assignment operation, either taking the address of an existing object or creating a new object. Both examples convert an instance of class D (on the right-hand side of the assignment operator) to an instance of class A (on the left-hand side). Notice that casting takes place automatically, without an explicit cast operator, making an upcast similar to a type promotion. It's easy to spot upcasting when implemented as an assignment operation.
  1. Taking the address of an existing object with the address-of operator and assigning it to a superclass pointer
  2. Dynamically creating a new object and assigning the address to a superclass pointer

In actual programs, upcasting is not as overt as in the above examples. Upcasting more commonly arises from function calls when a program passes an instance of a subclass to a function parameter of superclass type. It's far more difficult to recognize upcasting implemented this way.

A UML class diagram of shapes. Shape is the top class and has three subclasses: Circle, Rectangle, and Triangle. We refer to this inheritance hierarchy frequently throughout the chapter.
void render(Shape* s)
{
	.
	.
	.
}
    .
    .
    .
Circle* c = new Circle;
render(c);
(a)(b)
Upcasting by calling a function. Programs rarely perform an upcast operation with a simple assignment statement; they typically perform an upcast by passing an instance of a child class by-pointer to a function that "expects" an instance of a parent class. (a) illustrates a simple inheritance hierarchy while (b) demonstrates how the function call results in an upcast. The result of the function call has the same affect as the assignment statement Shape* s = new Circle;. The ability to replace or substitute an instance of a subclass (in the function call) for an instance of a superclass (in the function definition) is an important feature of inheritance called Substitutability.

Upcasting vs. Downcasting

It is also possible and sometimes necessary to cast "downwards" in an inheritance hierarchy. (Recall that it's not always possible to place superclasses above their subclasses, so we use "downwards" to mean "in the opposite direction of the inheritance arrows.") However, downcasting is somewhat riskier than upcasting and requires an explicit cast operation. So, upcasting is safe, and the compiler does it automatically, but downcasting is risky, and the compiler makes us explicitly perform the downcast. Chapter 2 introduced the casting operator and we extend it to classes and objects here.

Shape*	s = new Circle(0xff0000, 10);	// safe upcast
Circle*	c = (Circle *)s;		// unsafe downcast
Downcast example. Upcasting is always a safe operation, but downcasting can cause problems. For example, what happens if a programmer creates a Circle object, casts it to a Shape, and then downcasts it to a Rectangle? Downcasting is possible, but we must do it cautiously. A later section explores why upcasting is safe while downcasting is not.