Cla36: differentiate interface inheritance and implement inheritance

Source: Internet
Author: User

The concept of (public) Inheritance looks very simple. Further analysis shows that it consists of two parts: inheritance of function interfaces and Inheritance of function implementations. The differences between the two types of inheritance are exactly the same as the differences between the function declaration and the function definition discussed in this document.

As a class designer, you sometimes want the derived class to inherit only the interface (Declaration) of the member function. Sometimes, you want the derived class to inherit both the interface and Implementation of the function, but allow the class to be rewritten; sometimes you want to inherit both interfaces and implementations, and do not allow the derived class to rewrite anything.

To better understand the differences between these options, let's look at the class hierarchy below, which is used to represent the geometric shape in a graphic program:

class Shape {
public:
virtual void draw() const = 0;
virtual void error(const string& msg);
int objectID() const;
...
};

class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };

The pure virtual function draw makes shape an abstract class. Therefore, you cannot create an instance of the Shape class, but only an instance of its derived class. However, all classes inherited from shape (public) are greatly affected by shape, because:

  • The interfaces of member functions are always inherited. As stated in Clause 35, the meaning of public inheritance is "yes". Therefore, all the facts that are true to the base class must also be true to the derived class. Therefore, if a function applies to a class, it will certainly apply to its subclass.

The shape class declares three functions. The first function, draw, draws the current object on a certain painting surface. The second function, error, is called by other member functions to report error information. The third function, objectid, returns a unique integer identifier of the current object (Clause 17 provides an example of how to use this function ). Each function declares in a different way: draw is a pure virtual function; error is a simple (non-pure ?) Virtual function; objectid is a non-virtual function. What are the meanings of these different statements?

First, look at the pure virtual function draw. The most notable feature of pure virtual functions is that they must be re-declared in any specific class that inherits them, and they are often not defined in abstract classes. When we put these two features together, we will realize that:

  • The purpose of defining a pure virtual function is to make the derived class only inherit the function interface.

This makes sense for the shape: Draw function, because it is reasonable to make all shape objects be drawn, but the shape class cannot be shape :: draw provides a reasonable default implementation. For example, the algorithm used to draw an elliptical image is very different from the algorithm used to draw a rectangle. For example, the above shape: Draw declaration is like telling the subclass designer, "you must provide a draw function, but I don't know how you will implement it ."

By the way, defining a pure virtual function is also possible. That is to say, you can provide implementation for shape: draw, and the C ++ compiler will not block it, but the only way to call it is to specify which call is complete through the Class Name:

Shape * PS = new shape; // error! Shape is abstract


Shape * PS1 = new rectangle; // correct
PS1-> draw (); // call rectangle: Draw

Shape * PS2 = new ellipse; // correct
PS2-> draw (); // call ellipse: Draw

PS1-> shape: Draw (); // call shape: Draw
PS2-> shape: Draw (); // call shape: Draw

In general, apart from being able to impress your programmers at cocktail parties, it is generally not helpful to understand this usage. However, as we will see later, it can be applied as a mechanism to provide a default Implementation of "more secure than common" for simple (non-pure) virtual functions.

Sometimes it is useful to declare a class that does not contain anything except pure virtual functions. This class is called the Protocol class. It provides only function interfaces for the derived classes and is not implemented at all. The terms of agreement have been introduced in Clause 34 and will be mentioned again in Clause 43.

Simple Virtual functions are a little different from pure virtual functions. As an example, a derived class inherits the interface of a function, but a simple virtual function also provides an implementation. A derived class can choose to rewrite them or not rewrite them. Think for a moment to realize:

  • The purpose of declaring a simple virtual function is to make the derived class inherit the interface and default Implementation of the function.

Specific to shape: error, this interface is said, each class must provide a function that can be called when an error occurs, but each class can handle the error in any way they think appropriate. If a class does not want to do anything special, you can use the default error processing function provided in the shape class. That is to say, the shape: Error Declaration tells the subclass designer, "you must support the error function. But if you do not want to write your own version, you can use the default version in the shape class. "

In fact, it is dangerous to provide function declarations and default implementations for simple virtual functions. For more information, see the Plane hierarchy of XYZ airlines. XYZ has only two types of aircraft, A and B, and the flight modes of the two types are the same. Therefore, XYZ has designed such a hierarchy:

Class airport {...}; // represents an airplane
Class airplane {
Public:
Virtual void fly (const airport & Destination );
...
};

Void airplane: Fly (const airport & Destination)
{
Default Code for flying a plane to a destination
}

Class modela: Public airplane {...};
Class modelb: Public airplane {...};

Airplane: Fly is declared as virtual because fly functions must be supported by all aircraft and different aircraft models must be implemented in principle. To avoid repeated code writing in modela and modelb, the default flight behavior is provided by the airplane: Fly function. modela and modelb inherit this function.

This is a typical object-oriented design. The two classes share the same features (implementing the fly method). Therefore, this common feature is transferred to the base class and the two classes inherit this feature. This design makes the commonalities clear and avoids code duplication. In the future, it is easy to enhance functions and maintain features for a long time. All of these are highly praised by object-oriented technology. XYZ is really proud of it.

Now let us assume that Company XYZ has made a fortune and decided to introduce a new type of aircraft, type C. C is different from a and B. In particular, the flight mode is different.

XYZ programmers added a class for Type C in the above hierarchy, but they forgot to redefine the fly function because they were eager to put the new aircraft into use:

Class modelc: Public airplane {
... // The Fly function is not declared
};

Then they did something similar in the program:

Airport JFK (...); // JFK is an airport in New York City


Airplane * pA = new modelc;
...
Pa-> fly (JFK); // call airplane: fly!

This will cause a tragedy: the attempt to make the modelc object fly like modela or modelb. This kind of behavior can not be exchanged for passengers' trust in you!

The problem here is not airplane: fly has default behavior, but modelc can inherit this line without explicit declaration. Fortunately, it is easy to provide default behavior for sub-classes and give them only when the sub-classes want them. The trick is to cut off the connection between the virtual function interface and its default implementation. The following is a method:

Class airplane {
Public:
Virtual void fly (const airport & Destination) = 0;
...

Protected:
Void defaultfly (const airport & Destination );
};

Void airplane: defaultfly (const airport & Destination)
{
Default Code for flying a plane to a destination
}

Note airplane: fly has become a pure virtual function, which provides the flight interface. The default implementation still exists in the airplane class, but now it exists in the form of an independent function (defaultfly. If modela and modelb want to execute the default behavior, they simply make an inline call to defaultfly in their fly function bodies (for the relationship between inner and virtual functions, see Clause 33 ):

class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};

class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};

For modelc classes, it is impossible to inherit the incorrect fly implementation accidentally. Because the pure virtual function in airplane forces modelc to provide its own fly version.

Class modelc: Public airplane {
Public:
Virtual void fly (const airport & Destination );
...
};

Void modelc: Fly (const airport & Destination)
{
Code for modelc to a specific destination
}

This method won't be foolproof (programmers may also make mistakes due to "copy and paste"), but it is much more reliable than the original design. Airplane: defaultfly is declared as protected because it is indeed only the Implementation Details of airplane and Its Derived classes. Airplane users only care about flying planes, but not how they are implemented.

Airplane: defaultfly is also an important non-virtual function. Because no subclass will redefine this function, clause 37 illustrates this fact. If defafly fly is a virtual function, it will return to this question: if some subclasses should redefine defaultfly and forget to do so, what should we do?

Some people oppose separating interfaces from default implementations as separate functions, such as fly and defaultfly above. They believe that at least this will pollute the class namespace, because so many similar function names are spreading. However, they still agree that interfaces and default implementations should be separated. How can we solve this apparent conflict? You can use this fact: a pure virtual function must be declared again in the subclass, but it can still have its own implementation in the base class. The following airplane uses this to redefine a pure virtual function:

Class airplane {
Public:
Virtual void fly (const airport & Destination) = 0;
...
};

Void airplane: Fly (const airport & Destination)
{
Default Code for flying a plane to a destination
}

Class modela: Public airplane {
Public:
Virtual void fly (const airport & Destination)
{Airplane: Fly (destination );}
...
};

Class modelb: Public airplane {
Public:
Virtual void fly (const airport & Destination)
{Airplane: Fly (destination );}
...
};

Class modelc: Public airplane {
Public:
Virtual void fly (const airport & Destination );
...
};

Void modelc: Fly (const airport & Destination)
{
Code for modelc to a specific destination
}

This design is almost the same as the previous one, but the pure virtual function airplane: Fly replaces the independent function airplane: defaultfly. In essence, fly has been divided into two basic parts. Its Declaration illustrates its interface (the derived class must be used), and its definition describes its default behavior (the derived class may be used, but must be explicitly requested ). However, after the combination of fly and defaultfly, different protection levels can no longer be declared for these two functions: originally the protected code (in defaultfly) now it becomes public (because it is in Fly ).

Finally, let's talk about the shape non-virtual function, objectid. When a member function is a non-virtual function, its behavior in the derived class should not be different. In fact, a non-virtual member function represents a kind of special immutability, because it represents a behavior that will not be changed-no matter how special a derived class has. So,

  • The purpose of declaring a non-virtual function is to make the derived class inherit the interface and mandatory implementation of the function.

We can think that the declaration of shape: objectid is to say, "Each shape object has a function to generate an object identifier, and the object identifier is always generated in the same way. This method is determined by the shape: objectid definition. The derived class cannot change it. "Because a non-virtual function represents a kind of special immutability, it cannot be redefined in the subclass. Article 37 of this article is discussed.

After understanding the differences between pure virtual functions, simple virtual functions, and non-virtual functions in Declaration, You can precisely specify what you want the derived class to inherit: is it just an interface or an interface and a default implementation? Or, the interface and a forced implementation? Because these different types of declarations refer to different things, you must carefully choose between them when declaring a member function. Only in this way can we avoid two mistakes that programmers without experience often make.

The first error is to declare all functions as non-virtual functions. This makes the derived classes less specialized; non-virtual destructor are especially problematic (see clause 14 ). Of course, it is reasonable not to use the designed class as the base class (the m34 clause provides an example that you will do so ). In this case, it is appropriate to specifically declare a group of non-virtual member functions. However, all functions are declared as non-virtual functions. In most cases, they are caused by ignorance of the differences between virtual and non-virtual functions, or worry about the impact of virtual functions on program performance (see section M24 ). In fact, almost any class used as the base class has virtual functions (see clause 14 again ).

If you are worried about the overhead of the virtual function, let me introduce the 80-20 law (refer to the M16 clause ). It points out that in a typical program, 80% of the running time is spent on executing 20% of the Code. This law is very important because it means that, on average, 80% of function calls can be virtual functions, and they do not have even a negligible impact on the overall performance of the program. Therefore, before worrying about whether the virtual function overhead can be borne, you may wish to focus on the code that will actually affect the 20%.

Another common problem is to declare all functions as virtual functions. Sometimes this is true-for example, Protocol Class is evidence (see article 34 ). However, this often shows that class designers lack the courage to express their firm stance. Some functions cannot be redefined in a derived class. In this case, they must be explicitly declared as non-virtual functions. You cannot make your function seem to be able to do anything for anyone-as long as they spend some time redefining all functions. Remember, if there is a base class B, A derived class D, and a member function Mf, then each of the following calls to MF must work properly:

D * Pd = new D;
B * pb = Pd;


Pb-> MF (); // call MF through the base class pointer
Pd-> MF (); // call MF through the pointer of the derived class

Sometimes, you must declare MF as a non-virtual function to ensure that everything works as expected (see article 37 ). If you need the immutability of the particularity, let's say it!

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.