📄 ec6.htm
字号:
ps = pc; // ps's dynamic type is // now Circle*ps = pr; // ps's dynamic type is // now Rectangle*Virtual functions are dynamically bound, meaning that the particular function called is determined by the dynamic type of the object through which it's invoked: pc->draw(RED); // calls Circle::draw(RED)pr->draw(RED); // calls Rectangle::draw(RED)This is all old hat, I know; you surely understand virtual functions. (If you'd like to understand how they're implemented, turn to Item M24.) The twist comes in when you consider virtual functions with default parameter values, because, as I said above, virtual functions are dynamically bound, but default parameters are statically bound. That means that you may end up invoking a virtual function defined in a derived class but using a default parameter value from a base class: pr->draw(); // calls Rectangle::draw(RED)!In this case, pr's dynamic type is Rectangle*, so the Rectangle virtual function is called, just as you would expect. In Rectangle::draw, the default parameter value is GREEN. Because pr's static type is Shape*, however, the default parameter value for this function call is taken from the Shape class, not the Rectangle class! The result is a call consisting of a strange and almost certainly unanticipated combination of the declarations for draw in both the Shape and Rectangle classes. Trust me when I tell you that you don't want your software to behave this way, or at least believe me when I tell you that your clients won't want your software to behave this way.Needless to say, the fact that ps, pc, and pr are pointers is of no consequence in this matter. Were they references, the problem would persist. The only important things are that draw is a virtual function, and one of its default parameter values is redefined in a subclass.Why does C++ insist on acting in this perverse manner? The answer has to do with runtime efficiency. If default parameter values were dynamically bound, compilers would have to come up with a way of determining the appropriate default value(s) for parameters of virtual functions at runtime, which would be slower and more complicated than the current mechanism of determining them during compilation. The decision was made to err on the side of speed and simplicity of implementation, and the result is that you now enjoy execution behavior that is efficient, but, if you fail to heed the advice of this Item, confusing. 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: class Person { ... };class BankAccount {public: BankAccount(const Person *primaryOwner, const Person *jointOwner); virtual ~BankAccount(); virtual void makeDeposit(double amount) = 0; virtual void makeWithdrawal(double amount) = 0; virtual double balance() const = 0; ...};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: class SavingsAccount: public BankAccount {public: SavingsAccount(const Person *primaryOwner, const Person *jointOwner); ~SavingsAccount(); void creditInterest(); // add interest to 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: list<BankAccount*> allAccounts; // all accounts at the // bankLike 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, // a loop that won't compile (see below if you've never// seen code using "iterators" before)for (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { (*p)->creditInterest(); // error!}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: // a loop that will compile, but that is nonetheless evilfor (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { static_cast<SavingsAccount*>(*p)->creditInterest();}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: class CheckingAccount: public BankAccount {public: void creditInterest(); // add interest to account ...};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: for (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { if (*p points to a SavingsAccount) static_cast<SavingsAccount*>(*p)->creditInterest(); else static_cast<CheckingAccount*>(*p)->creditInterest();}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: class BankAccount { ... }; // as above// new class representing accounts that bear interestclass InterestBearingAccount: public BankAccount {public: virtual void creditInterest() = 0; ...};class SavingsAccount: public InterestBearingAccount { ... // as above};class CheckingAccount: public InterestBearingAccount { ... // as above};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: // better, but still not perfectfor (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { static_cast<InterestBearingAccount*>(*p)->creditInterest();}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: // all interest-bearing accounts in the banklist<InterestBearingAccount*> allIBAccounts;// a loop that compiles and works, both now and foreverfor (list<InterestBearingAccount*>::iterator p = allIBAccounts.begin(); p != allIBAccounts.end(); ++p) { (*p)->creditInterest();}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: class BankAccount {public: virtual void creditInterest() {} ...
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -