Back to Item 38: Never redefine an inherited default parameter value.   
  Continue to Item 40: Model "has-a" or "is-implemented-in-terms-of" through layering.

Item 39:  Avoid casts down the inheritance hierarchy.

In these tumultuous economic times, it's a good idea to keep an eye on our financial institutions, so consider a Protocol class (see Item 34) for bank accounts:

Many banks now offer a bewildering array of account types, but to keep things simple, let's assume there is only one type of bank account, namely, a savings account:

This isn't much of a savings account, but then again, what is these days? At any rate, it's enough for our purposes.

A bank is likely to keep a list of all its accounts, perhaps implemented via the list class template from the standard library (see Item 49). Suppose this list is imaginatively named allAccounts:

Like all standard containers, lists store copies of the things placed into them, so to avoid storing multiple copies of each BankAccount, the bank has decided to have allAccounts hold pointers to BankAccounts instead of BankAccounts themselves.

Now imagine you're supposed to write the code to iterate over all the accounts, crediting the interest due each one. You might try this,

but your compilers would quickly bring you to your senses: allAccounts contains pointers to BankAccount objects, not to SavingsAccount objects, so each time around the loop, p points to a BankAccount. That makes the call to creditInterest invalid, because creditInterest is declared only for SavingsAccount objects, not BankAccounts.

If "list<BankAccount*>::iterator p = allAccounts.begin()" looks to you more like transmission line noise than C++, you've apparently never had the pleasure of meeting the container class templates in the standard library. This part of the library is usually known as the Standard Template Library (the "STL"), and you can get an overview of it in Items 49 and M35. For the time being, all you need to know is that the variable p acts like a pointer that loops through the elements of allAccounts from beginning to end. That is, p acts as if its type were BankAccount** and the list elements were stored in an array.

It's frustrating that the loop above won't compile. Sure, allAccounts is defined as holding BankAccount*s, but you know that it actually holds SavingsAccount*s in the loop above, because SavingsAccount is the only class that can be instantiated. Stupid compilers! You decide to tell them what you know to be obvious and what they are too dense to figure out on their own: allAccounts really contains SavingsAccount*s:

All your problems are solved! Solved clearly, solved elegantly, solved concisely, all by the simple use of a cast. You know what type of pointer allAccounts really holds, your dopey compilers don't, so you use a cast to tell them. What could be more logical?

There is a biblical analogy I'd like to draw here. Casts are to C++ programmers what the apple was to Eve.

This kind of cast — from a base class pointer to a derived class pointer — is called a downcast, because you're casting down the inheritance hierarchy. In the example you just looked at, downcasting happens to work, but it leads to a maintenance nightmare, as you will soon see.

But back to the bank. Buoyed by the success of its savings accounts, let's suppose the bank decides to offer checking accounts, too. Furthermore, assume that checking accounts also bear interest, just like savings accounts:

Needless to say, allAccounts will now be a list containing pointers to both savings and checking accounts. Suddenly, the interest-crediting loop you wrote above is in serious trouble.

Your first problem is that it will continue to compile without your changing it to reflect the existence of CheckingAccount objects. This is because compilers will foolishly believe you when you tell them (through the static_cast) that *p really points to a SavingsAccount*. After all, you're the boss. That's Maintenance Nightmare Number One. Maintenance Nightmare Number Two is what you're tempted to do to fix the problem, which is typically to write code like this:

Anytime you find yourself writing code of the form, "if the object is of type T1, then do something, but if it's of type T2, then do something else," slap yourself. That isn't The C++ Way. Yes, it's a reasonable strategy in C, in Pascal, even in Smalltalk, but not in C++. In C++, you use virtual functions.

Remember that with a virtual function, compilers are responsible for making sure that the right function is called, depending on the type of the object being used. Don't litter your code with conditionals or switch statements; let your compilers do the work for you. Here's how:

Graphically, it looks like this:

Because both savings and checking accounts earn interest, you'd naturally like to move that common behavior up into a common base class. However, under the assumption that not all accounts in the bank will necessarily bear interest (certainly a valid assumption in my experience), you can't move it into the BankAccount class. As a result, you've introduced a new subclass of BankAccount called InterestBearingAccount, and you've made SavingsAccount and CheckingAccount inherit from it.

The fact that both savings and checking accounts bear interest is indicated by the InterestBearingAccount pure virtual function creditInterest, which is presumably redefined in its subclasses SavingsAccount and CheckingAccount.

This new class hierarchy allows you to rewrite your loop as follows:

Although this loop still contains a nasty little cast, it's much more robust than it used to be, because it will continue to work even if new subclasses of InterestBearingAccount are added to your application.

To get rid of the cast entirely, you must make some additional changes to your design. One approach is to tighten up the specification of your list of accounts. If you could get a list of InterestBearingAccount objects instead of BankAccount objects, everything would be peachy:

If getting a more specialized list isn't an option, it might make sense to say that the creditInterest operation applies to all bank accounts, but that for non-interest-bearing accounts, it's just a no-op. That could be expressed this way:

Notice that the virtual function BankAccount::creditInterest provides an empty default implementation. This is a convenient way to specify that its behavior is a no-op by default, but it can lead to unforeseen difficulties in its own right. For the inside story on why, as well as how to eliminate the danger, consult Item 36. Notice also that creditInterest is (implicitly) an inline function. There's nothing wrong with that, but because it's also virtual, the inline directive will probably be ignored. Item 33 explains why.

As you have seen, downcasts can be eliminated in a number of ways. The best way is to replace such casts with calls to virtual functions, possibly also making each virtual function a no-op for any classes to which it doesn't truly apply. A second method is to tighten up the typing so that there is no ambiguity between the declared type of a pointer and the pointer type that you know is really there. Whatever the effort required to get rid of downcasts, it's effort well spent, because downcasts are ugly and error-prone, and they lead to code that's difficult to understand, enhance, and maintain (see Item M32).

What I've just written is the truth and nothing but the truth. It is not, however, the whole truth. There are occasions when you really do have to perform a downcast.

For example, suppose you faced the situation we considered at the outset of this Item, i.e., allAccounts holds BankAccount pointers, creditInterest is defined only for SavingsAccount objects, and you must write a loop to credit interest to every account. Further suppose that all those things are beyond your control; you can't change the definitions for BankAccount, SavingsAccount, or allAccounts. (This would happen if they were defined in a library to which you had read-only access.) If that were the case, you'd have to use downcasting, no matter how distasteful you found the idea.

Nevertheless, there is a better way to do it than through a raw cast such as we saw above. The better way is called "safe downcasting," and it's implemented via C++'s dynamic_cast operator (see Item M2). When you use dynamic_cast on a pointer, the cast is attempted, and if it succeeds (i.e., if the dynamic type of the pointer (see Item 38) is consistent with the type to which it's being cast), a valid pointer of the new type is returned. If the dynamic_cast fails, the null pointer is returned.

Here's the banking example with safe downcasting added:

This scheme is far from ideal, but at least you can detect when your downcasts fail, something that's impossible without the use of dynamic_cast. Note, however, that prudence dictates you also check for the case where all the downcasts fail. That's the purpose of the final else clause in the code above. With virtual functions, there'd be no need for such a test, because every virtual call must resolve to some function. When you start downcasting, however, all bets are off. If somebody added a new type of account to the hierarchy, for example, but failed to update the code above, all the downcasts would fail. That's why it's important you handle that possibility. In all likelihood, it's not supposed to be the case that all the casts can fail, but when you allow downcasting, bad things start to happen to good programmers.

Did you check your glasses in a panic when you noticed what looks like variable definitions in the conditions of the if statements above? If so, worry not; your vision's fine. The ability to define such variables was added to the language at the same time as dynamic_cast. This feature lets you write neater code, because you don't really need psa or pca unless the dynamic_casts that initialize them succeed, and with the new syntax, you don't have to define those variables outside the conditionals containing the casts. (Item 32 explains why you generally want to avoid superfluous variable definitions, anyway.) If your compilers don't yet support this new way of defining variables, you can do it the old way:

In the grand scheme of things, of course, where you place your definitions for variables like psa and pca is of little consequence. The important thing is this: the if-then-else style of programming that downcasting invariably leads to is vastly inferior to the use of virtual functions, and you should reserve it for situations in which you truly have no alternative. With any luck, you will never face such a bleak and desolate programming landscape.

Back to Item 38: Never redefine an inherited default parameter value.   
  Continue to Item 40: Model "has-a" or "is-implemented-in-terms-of" through layering.