Effective C ++ 2e Item39

zhaozj2021-02-11  196

Terms 39: Avoid "down conversion" inheritance level

In today's an economic era, pay attention to our financial institution is a good idea. So, look at the protocol class of the bank account (see Terms 34):

Class Person {...};

Class BankAccount {public: BankAccount (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 variety of dazzling account types, but for simplification, we assume that there is only one bank account, called deposit account:

Class Savingsaccount: Public BankAccount {Public: Savingsaccount (Const Person * Joint Nowner); ~ Savingsaccount ();

Void CreditInterest (); // Add interest to your account

...

}

This is far from being a real deposit account, but it is still the sentence, what era is now? At least, it meets our current needs.

Bank wants to maintain a list for all of its accounts, which may be implemented through the List class template in the standard library (see Terms 49). Hypothesis list is called AlLaccounts:

List ALLACCOUNTS; // All accounts in the bank

Like all standard containers, List is stored in the object's copy, so in order to avoid multiple copies of each BankAccount, the bank decides to let Allaccounts save the pointer of BankAccount, not BankAccount itself.

Suppose it is now ready to write a code to traverse all accounts, calculate interest for each account. You will write this:

/ / Cannot be compiled (if you have never seen the code "iterative" code), see below) for (list :: Iterator P = allaccounts.begin (); p! = Allaccounts .end (); p) {

(* P) ​​-> CReditinterest (); // error!

}

However, the compiler will quickly let you realize that the pointer containing Allaccounts points to the BankAccount object, not the Savingsaccount object, so each cycle, P pointing to a BankAccount. This makes the call to CreditInterests invalid because Creditinterest is only declared for Savingsaccount objects, not BankAccount.

If "List :: Iterator P = ALLACCOUNTS.BEGIN ()" In your opinion, it is more like a noise in the phone line, not C , it is clear, you have used the container class in the C standard library before. template. This part in the standard library is often referred to as a standard template library (STL), and you can express its profile in terms 49 and m35. But now you only know, the variable P works like a pointer, which loops the elements in Allaccounts from the head to the end. That is, P works as if its type is BankAccount ** and the elements in the list are stored in an array. The above cycle cannot be discouraged by compilation. Indeed, AlLaccounts is defined as saving BankAccount *, but knowing that it is actually savingsaccount * in the above loop is Savingsaccount *, because SavingsAccount is the only class that can be in real way. Stupid compiler! It is even a stupid thing for us. So you decided to tell it: Allaccounts really contains Savingsaccount *:

/ / Can be compiled, but very bad for (List :: Iterator P = allaccounts.begin (); p! = Allaccounts.end (); p) {

Static_cast (* p) -> creditinterest ();

}

Everything is solved! Selling very clear, very beautiful, very simple, is just a simple conversion. You know what type of pointer is saved, the slow compiler does not know, so you have more reasonable things than this.

Here, I have to make a metaphor for the story of the Bible. Conversion to C programmers, just like Apple on Eve.

This type of conversion ---- From a base class pointer to a derived pointer - called "down conversion" because it converts the inherited hierarchy. In the example you just saw, the down conversion happens to work; however, as will be seen below, it will bring a nightmare to future maintenance staff.

Still returning to the topic of the bank. Successful incentives from deposit account business, bank decided to launch a check account business. In addition, assume that the check account is the same as the deposit account, but also interest:

Class CheckingAccount: Public BankAccount {public: Void Creditinterest (); // Add interest to your account

...

}

Needless to say, Allaccounts is now a list of two account pointers including deposits and checks. Then, the cycle of the calculated interest written above has a big hassle.

The first question is, although adding a CheckingAccount, but if you do not modify the loop code, compile can continue. Because the compiler simply listens to everything you tell (through Static_cast): * P Points Savingsaccount *. Who told you that it is its master? This will bring the first nightmare to future maintenance. The second nightmare during the maintenance period is that you must solve this problem, so you will write this code:

For (List :: Iterator P = Allaccounts.begin (); p! = allaccounts.end (); p) {if (* P pointing to a savingsaccount) static_cast (* p) - > creditinterest (); else stat_cast (* p) -> Creditinterest ();

}

At any time, I find yourself "If the object belongs to the type T1, do something; but if it belongs to the type T2, do something in a different thing", you have to fan yourself. This is not a C practice. Yes, in C, Pascal, or even Smalltalk, it is a very reasonable approach, but is not in C . In C , you want to use a virtual function.

remember? For a virtual function, the compiler can guarantee the correct function calls according to the type of object used. So don't throw the conditional statements or switching statements in your code; let the compiler for you. As follows:

Class BankAccount {...}; //

// A new class, indicating an account to pay interest, Class InterestBearingAccount: Public BankAccount {public: Virtual Void Creditinterest () = 0;

...

}

Class Savingsaccount: Public InterestBearingAccount {

... //

}

Class CheckingAccount: Public InterestBearingAccount {

... // AS ABOVE

}

Expressed with graphics as follows:

BankAccount ^ | InterestBearingAccount // / / / / / / / / / / / / / / CHECKINGACCOUNT SAVINGSACCOUNT

Because deposits and check accounts pay interest, it is naturally to transfer this common behavior to a public base class. However, if all bank accounts need to pay interest (in my experience, this is of course a reasonable hypothesis), you cannot transfer it to the BankAccount class. So, you have to introduce a new subclass of INTERESTBEARINGAATICCOUNT for BankAccount and make Savingsaccoun and CheckingAccount from it.

The fact that deposits and check accounts pay interest is reflected by InterestBearingAccount's pure virtual function Creditinterest, which is redefined in subclass savingsaccount and checkingaccount.

With new class hierarchies, you can rewrite the loop code like this:

// Some, but not perfect for (list :: item P = Allaccounts.begin (); p! = Allaccounts.end (); p) {static_cast (* P) -> creditinterest ();

}

Although this cycle contains an annoying conversion, the code is much more robust than the past, because even in the increase of the INTERESTBEARINGACCOUNT new sub-program, it can continue to work.

In order to completely eliminate the conversion, you must do some changes. One way is to limit the type of account list. If you can get a list of InterestBearingAccount objects instead of a BankAccount object, it is great:

// All accounts for paying interest in the bank are listed by list Allibaccounts;

/ / Can be compiled and can now operate in the future, which can work (List :: itrator p = allibaccounts.begin (); p! = Allibaccounts.end (); p) {

(* P) ​​-> Creditinterest ();

}

If you don't want to use the above "more specific list" method, let Creditinterest use to use all bank accounts, but for an account that does not have to pay interest, it is just an empty operation. This method can be represented in this way:

Class BankAccount {public: virtual void creditinterest () {}

...

}

Class Savingsaccount: Public BankAccount {...}; class checkingaccount: public bankaccount {...}; list ALLACCOUNTS; / / Look, no conversion! for (list :: itemator P = Allaccounts .begin (); p! = allaccounts.end (); p) {

(* P) ​​-> Creditinterest ();

}

It should be noted that the virtual function BankAccount :: CreditInterest provides an empty default implementation. This can be conveniently, its behavior is an air operation in the default; but this will also bring it difficult to foresee. If you want to know inside, and how to eliminate this danger, please refer to Terms 36. Also note that CreditInterest is a (implicit) inline function, which has no problem; but because it is also a virtual function, the inline command may be ignored. The clause 33 explained why.

As already seen above, "down conversion" can be eliminated by several methods. The best way is to replace this conversion with a virtual function, while it may not apply some classes, so each virtual function of these classes is an air operation. The second method is to enhance type constraints, so that there is no entry between the declaration type of the pointer and the true pointer type you know. In order to eliminate downward conversion, no matter how much workers are worthwhile, because of the difficulty of conversion, it is easy to cause errors, and the code is difficult to understand, upgrade, and maintain (see Terms M32).

At this point, what I said is fact; but, not all facts. In some cases, I really have to perform down conversion. For example, assuming or facing the situation starting with this Territor, that is, Allaccounts saves the BankAccount pointer, and Creditinterest is only defined for the SavingsAccount object, and write a loop to calculate interest for each account. Further assume that you can't change these classes; you can't change the definition of BankAccount, Savingsaccount or Allaccounts. (If they define this in a read-only library, this happens) If this is the case, you can only convert down, no matter how ugly.

Despite this, there is still a better way than the original conversion. This method is called "safe down conversion", which is implemented by the C Dynamic_CAST operator (see Terms M2). When using Dynamic_CAST for a pointer, first attempt to conversion, if successful (ie, the dynamic type of the pointer (see clause 38) and the type being converted), return new types of legitimate pointers; if Dynamic_cast fails, return empty pointer.

Here is an example of "safe down conversion":

Class BankAccount {...}; // and the same start

Class savingsaccount: // 同 PUBLIC BankAccount {...};

Class CheckingAccount: // Public BankAccount {...}.

List ALLACCOUNTS; / / It should be familiar with it ...

Void Error (const string & msg); // error handler; // see below

// Well, at least Convert Safety for (List :: Iterator P = Allaccounts.begin (); P! = Allaccounts.end (); P) {

// Try to convert * p security to savingsaccount *; // PSA definition information See below (Savingsaccount * PSA = Dynamic_Cast (* p)) {psa-> creditinterest ()

// Try to switch it secure to checkingaccount else if (checkingaccount * pca = Dynamic_cast (* p)) {PCA-> Creditinterest ();

// Unknown account type else {Error ("Unknown Account Type!");}}

This approach is not ideal, but at least the conversion failure can be detected, and Dynamic_CAST cannot be done. But pay attention to the situation of all conversion failures. This is exactly where the last ELSE statement is in the above code. With a virtual function, you don't have to make such an inspection, because each virtual function call will inevitably resolve to a function. However, once the conversion is intended, all the benefits are cultivated as there. For example, if a person adds a new type of account in the class hierarchy, but forgets the code above, all the conversions of it will fail. Therefore, it is important to handle this possible situation. In most cases, not all conversions will fail; however, once allowed to convert, good programmers will also touch trouble. The condition section of the above IF statement, some seem to have something defined by variables, see it rubbed the glasses? If you really do, don't worry, you haven't seen it wrong. This method of defining variables is to increase to the C language at the same time and Dynamic_CAST. This feature makes the code written more concise, because of the PSA or PCA, they only be used in the case of successful initialization by Dynamic_cast, it is not necessary to use the new syntax (including conversion) These variables are defined outside the conditional statements. (Terms 32 explain why usually avoid excess variable definitions) If the compiler does not support this new method of this defined variable, you can do it according to the old method:

For (List :: Iterator P = Allaccounts.begin (); p! = allaccounts.end (); p) {

Savingsaccount * PSA; // Traditional definition checkingaccount * pca; // Traditional definition

IF (PSA = Dynamic_Cast (* p)) {psa-> creditinterest ();

Else IF (PCA = Dynamic_Cast (* p)) {PCA-> Creditinterest ();

Else {Error ("Unknown Account Type!");}}

Of course, it is not very important to define such variables such as PSA and PCA from the importance of dealing with things. Important: Use the IF-THEN-ELSE programming to make down-converted more than the virtual function is much inferior, and this method should be used to use it. If you are lucky, your program will never see such a tragic lady.

转载请注明原文地址:https://www.9cbs.com/read-4445.html

New Post(0)