When working with objects that have an inheritance model you basically have an inverted tree that represents your object hierarchy. Contrary to a normal everyday trees, an inheritance tree has its root at the top. In other words, the root of the tree represents the base class and anything below it represents a more derived class.
If we move from a more specialised class to a less specialised class we are moving up the inheritance tree. Likewise, if we move from a less specialised class to a more specialised class we are moving down the inheritance tree. These two methods of traversing the inheritance tree are called up-casting and down-casting, respectively.
In Object Oriented Programming parlance, casting is a process of starting off with a concrete type and then referencing it via another type in its object inheritance hierarchy. As long as said reference is any type from the original concrete type back up through it’s blood-line to the ultimate base class this is a perfectly safe thing to do. Of course, if you try and case outside of the blood-line you are asking for trouble.
Up-casting is going from a derived class to a base class. This is always safe because we are moving from an object that has a more detail to an object that has less detail. It’s like a slim person putting on large clothes, they may be baggy but you can always get inside them.
The contrary is not true. When down-casting, going from a base class to a derived class, we are starting with an object that has less detail and trying to cast to an object that has more detail. It’s like being a large person trying to put on small clothes. You might be able to get them on but there is a real danger they might split and leave you exposed!
There is always a one-2-one relationship when up-casting, since a child only has one parent. This means we know we can safely cast from derived to base because we always know its parent. The same isn’t true when down-casting. A parent might have many children. How do we know that the one we’re about to down-cast two is the child we started off with? If we down-cast to the wrong child it is likely to have a tantrum!
Let’s consider an example to put this into some context. Supposed we have a base class called Animal. From that we derived two classes, one called Mammal and another called Reptile. From Mammal we derive Dog and Cat and from Reptile we derive Snake and Lizard. Our class hierarchy now looks like this:
+---------+ | Animal | +----+----+ | +---------+ | +---------+ | Mammal |<-----------+----------->| Reptile | +----+----+ +----+----+ | | +-------+ | +-------+ +---------+ | +----------+ | Dog |<--+-->| Cat | | Snake |<--+-->| Lizard | +-------+ +-------+ +---------+ +----------+
So, looking at this we can see that there are four clear blood-lines:
- Dog >> Mammal >> Animal
- Cat>> Mammal >> Animal
- Snake >> Reptile >> Animal
- Lizard>> Reptile >> Animal
If we start off with a Dog we can safely represent it via a Mammal reference or an Animal reference. Put into Object Oriented Programming parlance, we can say that a dog IS_A mammal and that a mammal IS_A(n) animal. The IS_A directive basically means that there is a inheritance relationship that models the concept that a derived class is just a more specialised base class.
In this case a dog is just a special mammal; it’s a mammal that just happens to have the additional characteristics of a dog. For that reason we can safely cast from a dog to a mammal because we’ just going to ignore the additional doggy type characteristics. Of course, because we started off with a dog and then cast to a mammal, we can safely cast back to a dog.
But what if we didn’t know this was the case? I mean, what if we don’t know that the original concrete type was a dog? Consider, I have a reference to a mammal but I have no idea whether it actually refers to a concrete type that is a dog or a concrete type that is a cat. Can I now safely cast it to a dog? What if it actually started out life as a cat? If I try and cast it to a dog it’s likely to end up with multiple personality disorder!
This is why down-casting can be so problematic. With an up-cast you always know what you’re getting because you know a derived type IS_A base type (it just has extra bells and whistles). The converse; however, is not true. A Dog may very well be a mammal but a mammal doesn’t necessarily have to be a dog.
So, the morel of this story is to avoid down-casting. It’s fraught with danger and is almost certainly going to end in tears unless you’re really (REALLY) careful! C++ does give you ways to do this safely (to be discussed in my next article) but just because you can do something doesn’t mean you should!
As it happens, there should rarely be a need to consider down-casting. If your class inheritance hierarchy has been designed properly you should never even need to care about down-casting. In fact, if you do find yourself needing to down-cast the chances are your class hierarchy is wrong and needs to be reviewed. The need to down-cast is nearly always a sign that your inheritance model is wrong.
Ask yourself, why do I need to do this down-cast? If it’s to get access to a member that isn’t in the current class but does exist in a more derived class then either you need to be using a more derived reference or you probably need to consider putting the member into the base class. If the latter doesn’t make sense it’s possible that what you need is an additional level of abstraction.
For example, we could have just had Animal as one class and then Dog, Cat, Snake and Lizard as derived classes from that but what if we then wanted to add a “lactate” method? With a few minor exceptions, only mammals lactate (as far as I know; I am not a zoologist!) so we could not put this in the base class Animal. We can; however, add this as a method in the Mammal class and then specialise it for Cat and Dog. We can then handle mammals differently from reptiles quite easily.
In my next article I’ll focus more specifically on C++ where I’ll explore how, for those very rare cases where it might be necessary, it is possible to safely down-cast. I’ll be looking at two methods: runtime type checking via dynamic_cast and compile time type checking via the visitor pattern.