Effective C ++ 2e Item34

zhaozj2021-02-11  216

Terms 34: Reduce compilation dependence between files to the lowest

Suppose you open your own C program code for a certain day, then make a small change to a class. Remind you that the change is not an interface, but an implementation of a class, that is, it is just a detail. Then you are ready to regenerate the program, you think, compile and link should only spend a few seconds. After all, just change a class! So you clicked "Rebuild" or entered Make (or other similar command). However, waiting for you is a horror, then it is painful. Because you find that the whole world is being recompiled, re-link!

When all this happens, isn't you just anger?

The reason happened because of the separation of the interface from implementation, C did not excellent. In particular, the C class definition includes not only interface specifications, but there are many implementation details. E.g:

Class Person {public: Person (Const String & Name, Const Date & Birthday, Const Country & Country; Virtual ~ Person ();

... // Simplification, omit the copy construction // function and assignment operator string name () const; string birthdate () const; string address () const;

Private: string name_; // Implement details Date BirthDate_; // Implement details address address_; // Implement details country citizenship_; // Implement details};

This is difficult to say is a very high design, although it shows a very interesting naming method: When the private data and public functions want to use a name to identify, let the former will take a tail to the end. . It is important to note that Person's implementation has been used, namely string, date, address, and country; Person wants to be compiled, you have to allow the compiler to access the definition of these classes. Such definitions are typically provided by the #include directive, so in the file header of the Person class, you can see the statement below:

#include // Used for String Type (see Terms 49) #include "Date.h" #include "address.h" #include "country.h"

Unfortunately, this, the compilation dependencies are established between the definition of Person files and these headers. So if any auxiliary class (ie String, Date, Address, and Country) change its implementation, or any class-dependent class changes, the file containing the Person class, and any files that use the Person class. Rebate. For users of the Person class, this is really annoying because the user is definitely a bundle.

So, you will be strange why c must put a class's implementation details in the definition of the class. For example, why can't I define this below, make the details of the category to separate? Class string; // "Concept" in advance String type // Please see Terms 49

Class Date; // Prevent Class Address; // Prevent Class Country; // Present Notice

Class Person {public: Person (Const String & Name, Const Date & Birthday, Const Country & Country; Virtual ~ Person ();

... // Copy constructor, operator =

String birthdate () const; s ;;;;;;;;;;;

If this method is feasible, then Person's users do not need to recompile unless the interface is changed. During the development of large systems, the interface tends to be fixed by the interface before the implementation of the class, so this interface and implementation will greatly save the time spent repeated and link.

Unfortunately, reality is always in contrast, see below, you will agree with this:

INT main () {int x; // Define an int

Person p (...); // Define a person // (for simplified parameters) ...

}

When you see the definition of X, the compiler knows that you must assign an int size memory. This is no problem, each compiler knows how much INT is. However, when seeing the definition of P, although the compiler knows that you must allocate a Person size memory, how do you know how much a Person object? The only way is to use the definition of classes, but if the definition of the class can omit the implementation details, how do the compiler know how much memory is the assignment?

In principle, this problem is not difficult to resolve. Some languages ​​such as SmallTalk, Eiffel and Java are handling this issue every day. Their approach is that when an object is defined, only the space that is sufficient enough to accommodate this object. That is, they correspond to the above code, they are like this:

INT main () {int x; // Define an int

Person * p; // Define a Person pointer ...}

You may have encountered such a code before because it is actually a legal C statement. This proves that programmers can do their own to "hide one object after the needle".

The following specifically describes how to adopt this technology to implement the Person interface and the implementation. First, only put the following things in the statement of the Person class:

// Compiler still knows these type names, // Because Person's constructor is used to use them Class String; // Do not do this for standard String, // Reference Terms 49Class Date; Class Center; Class Country; // class PersoniMPL will contain the real // exact details of the Person object, which is just the advance declaration of the class name Class Personimpl;

Class Person {public: Person (Const String & Name, Const Date & Birthday, Const Country & Country; Virtual ~ Person ();

... // Copy constructor, operator =

String birthdate () const; string address () const;

PRIVATE: PERSONIMPL * IMPL; // Point to the specific implementation class};

Now Person's user program is completely and String, Date, Address, Country, and Person's implementation detail. Those classes can be modified at will, while Person's users have a self-satisfaction, and they don't ask. More specifically, they do not need to recompile. In addition, because of the details of the implementation of Person, users cannot write code that relies on these details. This is the real interface and the separation of implementation.

The key to the separation is that "dependence on the definition" is replaced by "dependence on class declaration". So, in order to reduce compilation dependencies, we just know that this is enough: as long as it is possible, try not to rely on other files; if it is impossible, it is not possible to rely on class definitions. Other methods are stem from this simple design idea.

Here is the meaning of this idea directly deepening:

· If you can use the reference and pointer of the object, you should avoid using the object itself. Defining a reference and pointer will only involve this type of declaration. Objects that define this type require a type definition.

· Use the classes as possible without using the definition of the class. Because when a function is declared, if you use a class, it is absolutely no need to define this class, even if the function is passed and returned to this class:

Class Date; // Class Declaration

Date returnadate (); // correct ---- Do not need Date to define void Takeadate (Date D);

Of course, the pass value is usually not a good idea (see clause 22), but if you have to do this, don't cause unnecessary compilation dependencies.

If you are surprised when you have the definition of Returnadate and Takeadate in compile, please look at me with me. In fact, it didn't look so mystery, because anyone calls those functions, which will make Date's definition visible. "Hey" I know you are thinking, "Why do you want to declare a function that is called without people?" Is wrong! Not no one wants to call, but, not everyone will call. For example, suppose there is a library containing hundreds of functions (may involve multiple namespaces - see Terms 28), it is impossible to call each of the functions. The task of providing class definition (through a #include directive) is transferred from your function declaration header to a user file containing a function, you can eliminate the dependence of the type definition, and this dependent is unnecessary, it is human Caused. • Do not contain other header files in the header file, unless they are missing, unless they are missing. Instead, you have to declare the required class, let the users who use this header (through the #include directive) contain other header files, and the user code is ultimately compiled. Some users will complain that this is very inconvenient to them, but in fact you avoid many pains you have suffered. In fact, this technique is very respected and applied to the C standard library (see Terms 49); header file contains type declarations in the iostream library (and is just a type declaration).

The Person class only uses a pointer to point to an uncertain implementation, and such a class is often referred to as a Handle Class or a Envelope Class. (For the class pointed to by, the corresponding call is the body class (a letter class).) The letter class is called the letter class.) Occasionally, some people call "" CHESHIRE Cat, this has to mention the cat in "Aili Wizard Wheel, when it is willing, it will disappear in other parts of the body, just left a smile.

You will be curious about Bing Tang actually done something. The answer is simple: it just transfer all the function calls to the corresponding main class, the main class is truly completed. For example, the following is the implementation of the two member functions of Person:

#include "person.h" // Because it is implemented in the Person class, // must contain the definition of the class

#include "personimpl.h" // must also include the definition of the Personimpl class, / / ​​otherwise it cannot call its member function. // Note Personimpl and Person contain the same // member function, their interfaces are identical

Person :: Person (Const String & Name, Const Date & Birthday, Constly & Country) {Impl = New Personimpl (Name, Birthday, Addr, Country);

String Person :: Name () const {return Impl-> name ();} Please note how the constructor of the Person calls the constructor of the PersonImpl (Implicit to call, see Terms 5 and M8) and Person :: Name How to call Personimum :: Name. This is very important. Make Person a handle class does not change the behavior of the Person class, and changed only the location of behavior execution.

In addition to the handle, the other choice is to make Person a special type of abstract base class called a protocol class. Depending on the definition, the protocol class is not implemented; it exists to determine an interface for derived classes (see Terms 36). So, it generally has no data member, no constructive function; there is a false prevention function (see Terms 14), there is a set of pure virtual functions for developing an interface. The Person's 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;

Users of the Person class must use it through the Person's pointer and reference, because instantiation a class containing a pure virtual function (however, you can instantiate the Person's derived class - see below). Like the user of the handle, the user of the protocol only needs to recompile if the class interface is modified.

Of course, users of the protocol must have any way to create a new object. This often implements a function, this function plays the role of constructor, and the class of this constructor is the derived class that is really instantiated. This function is quite a large (as factory function, virtual constructor), but behavior is the same: returns a pointer, this pointer points to the dynamic allocation of the support protocol class interface (see clause M25) . Such a function is like this:

// MakePerson is a "fiction" ("Factory Function") Person * MakePerson ("Factory Function") ("Factory Function") ("Factory Function") (Const String & Name), which supports the Person interface. , Then const address & addr, // Return to the object pointer const country & country);

The user uses this:

String name; Date DateOfbirth; country nation;

...

// Create a object that supports the Person interface Person * PP = MakePerson (Name, DateOfbirth, address, nation);

...

Cout << pp-> name () // Use the Person interface to use the object << "WAS Born on" << pp-> birthdate () << "and now lives at" << pp-> address (); .. .

Delete PP; // Remove Object

MakePerson This type of function and the protocol class corresponding to the object it created (the object support this protocol class interface) are closely linked, so it is a good habit of declaring it as a static member of the protocol class:

Class Person {public: ... //

// MakePerson is now a member Static Person * MakePerson (Const String & Name, Const Date & Birthday, Const Address & Addr, Const Country & Country);

This will not bring confusion to the global namespace (or any other name space), because this nature has a lot of functions (see Terms 28).

Of course, in a certain place, a Concrete Class, which supports the protocol interface interface is inevitably defined, and the true constructor must be called. They all occur behind implementation files. For example, the protocol class may have a derived specific class RealPerson, which implements the inherited virtual function:

class RealPerson: public Person {public: RealPerson (const string & name, const Date & birthday, const Address & addr, const Country & country): name_ (name), birthday_ (birthday), address_ (addr), country_ (country) {}

Virtual ~ realperson () {}

String name () const; // The specific implementation of the function does not have string birthdate () const; // here, but String Address () const; // is easy to implement String nationality () const;

Private: String name_; agent birthday_; address address_; country country_;

With RealPerson, Write Person :: MakePerson is a snatch:

Person * Person :: MakePerson (Const String & Name, Const Date & Birthday, Const ADDR, Const Country & Country) {Return New RealPerson (Name, Birthday, Addr, Country);

The implementation of the protocol has two most common mechanisms, and RealPerson demonstrates one of them: first from the protocol class (PERSON) inherit the interface specification, then implement the function in the interface. Another mechanism for implementing the protocol class involves multiple inheritance, which will be the topic of clause 43. Yes, the handle and protocols have separated interfaces and implementations, thereby reducing the dependence of inter-file compilation. "But how much is all of these tricks?", I know that you are waiting for a ticket. " The answer is the most common sentence in computer science field: it will meantone more than the time at runtime, and it will also meant memory.

In the case of a handle, the member function must obtain object data through (pointing to the implementation) pointer. In this way, the indirectibility of each access is one layer. In addition, this pointer should also be counted in calculating the memory size occupied by each object. Also, the pointer itself has to be initialized (within the constructor of the handle), so that it is directed to the implementation object that is dynamically allocated, so it is also responsible for the overhead of dynamic memory allocation (and subsequent memory release). ---- See Terms 10.

For protocol classes, each function is a virtual function, and all the overhead of indirect jump (see Terms 14 and M24) each time the function is called. Moreover, each object that is derived from the protocol will inevitably contain a virtual pointer (see Terms 14 and M24). This pointer may increase the amount of memory required for the object storage (depending on: For the virtual function of the object, this protocol class is not the only source thereof).

The last point, the handle and the protocol category do not usually use the inline function. Access to implementation details when using any inner function, and the original intention of design handle and protocols is to avoid this.

But if only the handle and protocols will bring them into the cold palace, it is wrong. Just like the virtual function, will you not need them? (If you answer, then you are watching a book that you shouldn't see!) Instead, you should use these technologies in development. During the development phase, try to reduce the negative impact of "implementation" to the user's negative impact on the "implementation" change. If the degree of increase in speed and / or volume is much greater than the degree of dependence between the class, then use a specific class to replace the handle and protocol class when the program is transformed into a product. I hope that one day will have a tool to automate such transformations.

Some people also like to mix the handle, protocols, and specific classes, and use very well. This is essential to run efficient, easy to improve, but there is a big shortcoming: or must try to reduce the time consumption when the program is reconfined.

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

New Post(0)