Effective C ++ 2e Item43

zhaozj2021-02-11  268

Terms 43: Inheritance

Who is going to see, inheritance (MI) either it is considered to be a god pen, or it is used as a devil. Supporters advocate that it is necessary for natural modeling for real world problems; and critics argue that it is too slow, it is difficult to achieve, but the function is not strong. What is more embarrassing is that there is still a difference between the object-oriented programming language. It is still between C , Eiffel and The Common Lisp Object System (CLOS) provides MI; SmallTalk, Objective C and Object Pascal are not available; while Java is just Provide limited support. Who should a poor programmer believe?

Before I believe anything, I first get the facts. In C , the fact that Mi is not arguing is that MI's appearance is like opening Pan Dora's box, bringing a complexity that does not exist in single inheritance. Among them, the most basic one is an erliness (see Terms 26). If a derived class has inherited a member name from multiple base classes, all access to this name is second; you must clear which member you refer to. The following example is taken from a special discussion in ARM (see Terms 50):

Class Lottery {public: Virtual Int Draw ();

...

}

Class graphicalObject {public: Virtual int draw ();

...

}

Class Lotterysimulation: Public Lottery, Public GraphicalObject {

... // No declaration DRAW

}

Lotterysimulation * PLS = New Lotterysimulation;

PLS-> DRAW (); // Error! ---- 二 义 PLS-> Lottery :: Draw (); // Correct PLS-> GraphicalObject :: Draw (); // Correct

This code looks very awkward, but at least you can work. Unfortunately, it is difficult to avoid this clumsy. Even if one of the inherited DRAW functions is private member, it is still possible to be accessed. (There is a good reason to explain this, but the full description is provided in terms 26, so it is no longer repeated.)

Explicitly restricting the modified members not only clumsy, but also limits. When explicitly uses a class name to limit the modification of a virtual function, the behavior of the function will no longer have virtual characteristics. On the contrary, the called function can only be the one you specified, even if the call is active on the object of derived class:

Class Speciallotterysimulation: Public Lotterysimulation {public: Virtual Int Draw ();

...

}

PLS = New Speciallotterysimulation;

PLS-> DRAW (); // Error! ---- still have secondary pls-> lottery :: Draw (); // call Lottery :: DrawPLS-> graphicalObject :: Draw (); // call graphicalObject: : DRAW

Note that in this case, even if the PLS points to the Specialtterysimulation object, it is impossible (no "down conversion" --- See Terms 39) Call the DRAW function defined in this class. Didn't finish it, is there. The DRAW function in Lottery and GraphicalObject is declared as virtual functions, so subclasses can redefine their (see clause 36), but if LotterysImulation wants to redefine it for both? Defusion is that this is impossible, because a class only allows a single function that does not have a parameter, named DRAW. (This rule has an exception, that is, a function is constant and the other is not time --- See clause 21)

From a certain aspect, this problem is very serious, serious enough to become a reason for modifying C languages. One possibility is discussed in ARM, ie, the virtual function that is inherited can be "renamed"; but later discovery, you can subtly avoid this problem by adding a new class:

Class auxlottery: public lottery {public: virtual int lotterydraw () = 0;

Virtual int draw () {return lotterydraw ();}};

Class auxgraphicalobject: public graphicalObject {public: Virtual int graphicalObjectdraw () = 0;

Virtual int draw () {return graphicalObjectdraw ();}};

Class Lotterysimulation: Public auxlottery, public auxgraphtery, public auxgraphterybject {public: Virtual int ketterydraw (); virtual int graphicalobjectdraw ();

...

}

These two new classes, Auxlottery and AuxgraphiPhicalObject, in essence, declare new names for their inherited DRAW functions. The new name is provided in the form of a pure virtual function. In this example, the LotteryDraw and GraphhicalObjectDraw; functions are pure virtual, so specific subclasses must redefine them. In addition, each class redefines the inheritable DRAW function, allowing them to call new pure virtual functions. The final effect is that in this type of architecture, a single name DRAW with a second sense is effectively divided into two names that are unlimited but functional equivalents: LotteryDraw and GraphicalObjectDRAW:

Lotterysimulation * PLS = New Lotterysimulation;

Lottery * pl = pls; graphicalobject * PGO = PLS;

// Call Lotterysimulation :: LotteryDrawPL-> DRAW ();

// Call Lotterysimulation :: graphicalObjectdrawpgo-> DRAW ();

This is a method of integrated a pure virtual function, a simple virtual function, and an inline function (see Terms 33), which is worth keeping in mind. First of all, it solves the problem, this question can be encountered. Second, it can remind you that multi-inheritance can lead to complexity. Yes, this method solves the problem, but just to redefine a virtual function, you have to introduce new classes, are you really willing to do this? Auxlottery and AuxGraphicalObject classes are essential for ensuring the correct operation of the class hierarchy, but they do neither an abstraction of Problem Domain, nor does it correspond to an abstraction of implementation Domain. They are simply existed as an implementation, and there is no other place. You must know that good software is "unrelated", this law is also applicable. In the future, MI will face more questions, and the second-meaning problem (even interesting) is just just start. Another problem is based on such practical experience: a inheritance hierarchy of the first elephant:

Class B {...}; Class C {...}; Class D: Public B, PUBLIC C {...};

B C / / / / / D

Often the last tragic development of the imaging:

Class A {...}; Class B: Virtual Public A {...}; Class C: Virtual Public A {...}; Class D: Public B, PUBLIC C {...};

A / / / / / / b C / / / / / / / D

Diamonds may be the best friend of the girl, maybe not; but affirm that the inheritance structure of a diamond shape is definitely impossible to be our friend. If you create a hierarchy like this, you will face this question immediately: Is it a becoming a virtual base class? That is, is the inheritance from A should be virtual? In reality, the answer is almost always ---- should; only the object of the type D will contain multiple copies of the data member of the type D. It is to recognize this fact that B and C above will declare A as a virtual base class.

Unfortunately, when defining B and C, you may not know if there will be classes in the future, and know that this is not necessary to correctly define these two classes. For the designer of the class, this is really difficult. If A declares that A declarations of B and C, the designer of the future D will need to modify the definition of B and C in order to use them more effectively. Usually, this is difficult to do because definitions of A, B and C are often read-only. For example, such a situation: A, B and C are written in a library, while D is written by the library.

On the other hand, if A declares a definiteness of B and C, it is often imposed on the space and time to add additional overhead. Because the virtual base class is often implemented by the object pointer, not the object itself. It is not necessary to say that the distribution of objects in memory is related to the compiler, but a constant fact is that if a as a "non-virtual" base class, the distribution of objects in the memory usually occupies continuous memory cells; If A is a "virtual" base class, sometimes, the distribution of type D is distributed in memory, but two units contain pointers, pointing to memory units containing virtual foundation data: A is non-virtual Typical memory distribution of D object when the base class:

Part A B Part A Part C Part D Part

A is the memory distribution of D object in some compiler when the virtual base class:

------------------------------------------------ | | | B Part Pointer C Part Pointer D Part A Some | | | --------------------------------------------------------------------------------------------------------------------

Even if the compiler does not use this special implementation strategy, use virtual success usually brings penalties on a certain space.

Considering these factors, it seems that if MI is involved in the efficient class design, it has a extraordinary perspective. However, now, common sense is increasingly becoming a rare product, so you will not understand the language characteristics, not only require designers to get the future needs, but it is simply to make a thorough prophet (See the Terms M32).

Of course, this can also be said to be a virtual function and a non-virtual function, but there is still a significant difference. Terms 36 Description, the virtual function has a defined advanced meaning, and the non-virtual function also has a clear high-level meaning of definition, and its meaning is significantly different, so it is clear that it is clear that the designer of the subclass will convey something The choice is possible between the two is possible. However, determining whether the base class should be virtual, the lack of definition of clear advanced meanings; instead, decision is usually the hierarchy of the entire inheritance, so unless you know the entire hierarchy, you cannot make a decision. If you need to know how to use it correctly, you need to use it in the future, it will be difficult to design an efficient class.

Even if an unsatisfactory problem is avoided, there will be a number of complex issues waiting for you if you have doubts that should be inherited from the base class. For the long-distance short, I will only ask the other two points that should be remembered:

· Pass the constructor parameters to the virtual base class. When non-virtual inheritance, the parameters of the base class constructor are specified by the member initialization list of members of the derived class. Since the hierarchy of the single inheritance requires only non-virtual basis, the upload transmission of the parameters in the inheritance hierarchy is a natural way: the class of the N-layer pass the parameters to the N-1 layer. However, the constructor of the virtual base class is different, and its parameters are specified by the list of member initialization lists of the most underlying class in the inheritance structure. This is caused by initializing the category of the virtual base class. It may be far away from it in the inheritance map; if new classes increase into the inheritance structure, the initialization class may also change. (A good way to avoid this problem is to eliminate the need for virtual base class delivery constructor parameters. The easiest way is to avoid placing data members in such a class. This is essentially Java solution: Java The virtual base class (ie, "interface") is forbidden to include data) · The priority of the virtual function. Just when you think that you have made all the second sense, they turn around you in front of you. Take a look at the inheritance diagram of diamond shapes about classes A, B, C and D. Suppose A defines a virtual member function MF, C redefined it; B and D have no redifunity MF:

A Virtual Void Mf (); // / / / b c Virtual Void Mf (); / / / // D

Based on the previous discussion, you will think that there are two righteous:

D * pd = new d; pd-> mf (); // a :: mf or c :: mf?

Which MF is called for D's object, is the one that is inherited from C or indirect (by b) from A.? The answer depends on how B and C are inherited from a. Specifically, if a is the non-virtual basis of B or C, the call has an amphony; but if A is the virtual base class of B and C, it can be said that the redefinization of MF in C is higher than the initial A. The definition is thus parsing (unsteading) to C :: MF through the PD call to MF. If you think about it, think about it, this is the behavior you want; but you need to sit down and think about it. It is indeed a pain.

Perhaps this you will recognize that MI does result in complication. Maybe you realize that everyone does not want to use it. Maybe you are ready to recommend that the International C Standards Committee will inherit from the language; or at least you want to suggest to your boss, the company's programmers are prohibited from using it.

Maybe you are too worried.

Keep in mind that C designers don't want to make more inheritance, it is difficult to use; it is just that you can work with more reasonable ways, which itself brings certain complexity. The above discussion you will notice that many of these complexities are caused by the use of virtual base classes. If you can avoid using a virtual base class - i.e., if you can avoid the fatal diamond shape inheritance map ---- things will be more reasonable.

For example, the provisions 34 say that the presence of protocol classes is only developed for derived classes; it does not have data members, there is no constructor, there is a false prevention function (see Terms 14), a set of specified The pure virtual function of the interface. A Person protocol looks like this:

Class Person {public: Virtual ~ Person ();

Virtual string name () const = 0; Virtual string birthdate () const = 0; Virtual string address () const = 0; Virtual string nationality () const = 0;}; This class must use Person's pointer when programming Or references because the abstract class cannot be instantiated.

In order to create an object that "can be used as a Person object", Person's users use the factory function (Factory Function, see Terms 34) to instantiate specific subclasses:

// Factory function, creates a Person object from a unique database ID // (DatabaseId Personidentifier);

Databaseid askuserfordatabaseId ();

DatabaseId PID = askUserFordatabaseId ();

Person * pp = makeperson (PID); // Create an object that supports the Person // interface

... // Operation via Person's Member Function / Operation * PP

Delete PP; // Remove the object that no longer needs

This brings a question: How do you be created by the object pointed to the pointer returned by MakePerson? Obviously, some specific classes must be derived from Person, making MakePerson to instantiate them.

Suppose this class is called MyPerson. As a concrete class, MyPerson must implement a pure virtual function inherited from Person. This can start from zero, but if there are some components that can do most or all of the required work, then from the perspective of software engineering, they can use these components. For example, suppose that there is already an old PersonInInfo related to the database, it is available for MyPerson:

Class Personinfo {Public: Personinfo (DatabaseId PID); Virtual ~ Personifo ();

Virtual const char * tell * thebirthdate () const; virtual const char * theaddress () const; virtual const char *.

Virtual const char * valueDelimopen () const; // Look at Virtual const char * valueDelimclose () const;

...

}

It can be concluded that this is an old class because its member function returns const char * instead of the String object. But if you don't wear it, why not wear? This class's member function name suggests that this pair of shoes will be very comfortable.

Then you will find that the original design of PersonInfo is used to easily print the database field in a variety of different formats, and the beginning and end of each field value are separated by special strings. By default, the start division of the field value and the end separator are parentheses, so the field value "Ring-TAILED Lemur" will be formatted:

[Ring-TAILED Lemur]

Because brackets are not all of the PersonInfo users you want, virtual functions VALUEDELIMOPEN and VALUEDEDELIMCLOSE allow derived classes to specify their own initiator and end separator. The implementation of the PersonInfo class, thebirthdate, theaddress, and the Internationality will call these two virtual functions to add an appropriate separator to their return values. Take PersonInfo :: Name as an example, the code looks like this: const char * personfo :: ValuedEnin () const {return "["; // default start separator}

Const char * personfo :: valuedelimclose () const {return "]"; // Default end separator}

Const Char * Personinfo ::1ame () const {// The buffer is retained for the return value. Because it is a static // type, it is automatically initialized to all zones. Static char value [MAX_FORMATTED_FIELD_VALUE_LENGTH];

// Write start division strcpy (value, valuedelimopen ());

Add the name field value of the object to the string

// Write end separator strcat (value, valuedelimclose ());

Return Value;

Some people will pick Personinfo :: The design (especially using fixed-size static buffers ---- see Terms 23), but please put your pick-up, pay attention to this: First, thename calls ValuedEldelimopen, Generate the start division of the string that will return; then, generate the name value itself; finally, call ValuedElimClose. Because Valuedelimopen and ValuedLimClose are virtual functions, the result returned by Thename relies on PersonInfo, which depends on the class derived from Personinfo.

As the implementation of MyPerson, this is a good news because you find that Name and its related functions need to return without modified values ​​when studying the rules of the Person document, that is, the separator is not allowed. That is, if a person comes from Madagascar, calling this person's nationality function will return "Madagascar" instead of "[Madagascar]".

The relationship between MyPerson and Personinfo is that PersonInfo just some functions make MyPerson easy to implement. Second only. Didn't see a relationship with "it is a" or "there is a". Their relationship is "implementation with ..." and we know that this can be represented by two ways: through the layering (see clause 40) and through private inheritance (see clause 42). Terms 42 pointed out that the layering generally is a better way, but in the case where there is a virtual function to be redefined, private inheritance is required. The current situation is that MyPerson needs to redefine Valuedelimopen and ValuedEldelimClose, so they cannot be hierarchical, but must be inherited with private inheritance: MyPerson must inherit from Personinfo.

But MyPerson must also implement the Person interface, which requires public inheritance. This has led to a very reasonable application: combine the private inheritance and implementation of the interface: Class Person {// This class specifies public: // Need to be implemented, Virtual ~ Person (); // interface

Virtual string name () const = 0; Virtual string birthdate () const = 0; Virtual string address () const = 0; Virtual string nationality () const = 0;

Class DatabaseId {...}; // is used later; // Details are not important

Class PersonInfo {// This class is a useful public: // function, which can be used to PersonInInfo (DatabaseId PID); // implement the Person interface Virtual ~ PersonInfo ();

Virtual const char * tell * thebirthdate () const; virtual const char * theaddress () const; virtual const char *.

Virtual const char * valuedelimopen () const; virtual const char * valuedelimclose () const;

...

}

Class myperson: public person, // Note, use private personInInfo {// Multi inheritance public: MyPerson (DatabaseId PID): PersonInfo (PID) {}

// Inherited Demand Definition Const Char * ValuedEldelimopen () Const CHAR * VALUEDELIMCLOSE () Const {Return ";}

// Realize the Reed Name () Const {Return Personifo ::1EName ();

String birthdate () const {return personfo :: thebpterdate ();}

String address () const {return personfo :: THEADDRESS ();

String nationality () const {return personfo :: thenationality ();}}

Expressed with graphics, it looks like this:

Person Personinfo / / / / // myperson

This example demonstrates that MI will be both useful and easy to understand, although the terrible diamond shape inheritance is not significantly disappeared.

However, you must be careful. Sometimes you will fall into such a trap: For some inheritance hierarchies that need to be changed, this is better to use a more basic redesign, but you use MI to pursue speed. For example, assume that a class hierarchy is designed for an active cartoon role. At least in terms of concept, it will make fun of various roles to dance singing, but each role is not the same. In addition, the default behavior of dancing singing is nothing. All of these use C is like this:

Class CartoonCharacter {public: Virtual Void Dance () {} Virtual Void Sing ()}};

The virtual function naturally embodies such a constraint: singing dancing makes sense for all CartoonCharacter objects. What does not do the default behavior represents an empty definition of those functions in the class (see Terms 36). Suppose there is a special type of cartoon role is a dragonfly, it dances in a special way:

Class Grasshopper: Public CartoonCharacter {public: Virtual void Dance (); // What is otherwisled in other parts of Virtual Void Sing (); // Define What else}

Now, after the Grasshopper class is implemented, you want to add a class:

Class Cricket: Public CartoonCharacter {public: Virtual void Dance (); Virtual Void Sing ();

When you sit down and implement the cricket class, you realize that many of the code written for the GrassHopper class can be reused. But this requires a point of charge, because it is necessary to find out the difference between the dragonfly and the singing dance. You suddenly came up with a good way to reuse: You are ready to use the Grasshopper class to implement the cricket class, and you are also ready to use the virtual function to make the cricket class can customize the grashopper behavior.

You immediately realize the relationship between these two requirements - "to implement" and redefine the ability to redefine the virtual function - meaning that cricket must inherit from Grasshopper, but the cricket is certainly a cartoon role So you redefine cricket by inheriting from Grasshopper and CartoonCharacter inheritance:

Class Cricket: Public CartoonCharacter, Private Grasshopper {public: Virtual Void Dance (); Virtual Void Sing ();

Then prepare the necessary modifications to the Grasshopper class. In particular, you need to declare some new virtual functions to redefine cricket:

Class grasshopper: public cartooncharacter {public: virtual void dance (); virtual void start ();

Protected: Virtual Void Dancecustomization1 (); Virtual Void DanceCustomization2 ();

");

蜢 Dancing is now defined as this:

Void grasshopper :: dance () {Perform a common dancing action;

Dancecustomization1 ();

Execute more common dangers;

DanceCustomization2 ();

Perform the final common dance action;}

The design of the singles is similar to this.

Obviously, the cricket class must be modified because it must redefine the new virtual function: Class Cricket: Public CartoonCharacter, Private grasshopper {public: Virtual void Dance () {grasshopper :: dance ();} Virtual void Grasshopper :: Sing ();

Protected: Virtual Void Dancecustomization1 (); Virtual Void DanceCustomization2 ();

");

This seems very good. When the cricket object is required to dance, it performs a common DANCE code in the Grasshopper class, then performs the DANCE code customized in the cricket class, and then proceeds to the code in Grasshopper :: Dance, and so on.

However, this design has a serious flaw, this is, you accidentally hit the "Okham razor" ---- any Okham razor is harmful to the thoughts, especially the William of Occam. Okham advocates: If there is no need, do not increase the entity. In the case of now, the entity refers to the inheritance relationship. If you believe that more inheritance is more complicated than inheritance (I hope you believe), the design of the cricket class is not necessary. (Translation: 1) William of Occam (1285-1349), Britishist, philosopher. 2) Occam's Razor is a thought, mainly proposed by William of Occam. The reason why it is called "Okham razor" is because William of Occam often uses this idea very sharply. )

The foundation of the problem is that there is not "implementation" between the CRICKET class and the Grasshopper class. Instead, there is a common code between the CRICKET class and the Grasshopper class. In particular, they have a code that decides to sing dancing behavior ---- 蜢 and 蟀 has this common behavior.

Say that two classes have a common way to inherit a class from another class, but let them all inherit from a common base class, the public code between the 蜢 and 蟋蟀 is not a grasshopper class, nor by cricket, But belongs to their common new base, such as Insect:

Class Cartooncharacter {...};

Class INSECT: PUBLIC CARTOONCHARACTER {public: Virtual void Dance (); // 蜢 and 蟀 Virtual void start (); //

Protected: Virtual void Dancecustomization1 () = 0; Virtual Void DanceCustomization2 () = 0;

Virtual void singcustomization () = 0;

Class grasshopper: public insect {protected: virtual void dancecustomization1 (); Virtual void Dancecustomization2 ();

");

Class Cricket: Public Insect {Protected: Virtual Void DanceCustomization1 (); Virtual Void DanceCustomization2 (); Virtual Void Singcustomization ();

Cartooncharacter | | INSECT / / / / / GRASSHOPPER CRICKET

It can be seen that this design is clearer. Just involve single inheritance, in addition, it is only used in public inheritance. Grasshopper and cricket definitions are just customizations; they have inherited Dance and Sing functions from INSECT. William of Occam will be very proud.

Although this design is clearer than the scheme of MI, the first look may feel more inferior to the use of MI. After all, this single inheritance structure introduces a new class in this single inheritance structure, and does not need to use MI. If there is no need, why should I introduce an extra class?

This will bring you more inheritance of the introspective nature. The surface seems easier to use. It does not need to add new classes, although it requires adding some new virtual functions in the Grasshopper class, but these functions are increasing in any case.

Imagine a programmer is maintaining a large C class library, now you need to add a new class in the library, just like the Cricket class to be added to the existing CartoonCharacter / Grasshopper hierarchy. The programmer knows that there is a large number of users using the existing hierarchy, so the greater the change in the library, the greater the impact on the user. The programmer is determined to reduce this impact to a minimum. After the three considers of various options, the programmer recognizes that if an additional private inheritance connection from Grasshopper to Cricket, any other change will not be required. The programmer can't help but expose this idea to smile, it is clear that it can increase the function in the future, while the cost is only a small complexity.

Now I want to be the programmer responsible for maintenance is you. So, please resist this temptation!

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

New Post(0)