Minimize compilation dependencies between files

Source: Internet
Author: User

You enter your program and make minor changes to the implementation of a class. Remind you that it is not a class interface, but an implementation, but a private thing. Then you re-build the program, which is expected to take only a few seconds. After all, only one class is changed. You click on build or type make (or other equivalent behaviors). Then you are stunned and depressed, just as you suddenly realize that the whole world is re-compiled and connected! When such a thing happens, don't you hate it?

The problem is that C ++ does not do a good job of extracting interfaces from implementation. A class definition not only specifies a class interface, but also has a considerable number of implementation details. For example:

Class person {
Public:
Person (const STD: string & name, const Date & birthday, const address & ADDR );
STD: string name () const;
STD: String birthdate () const;
STD: String address () const;
...

PRIVATE:
STD: String thename; // implementation detail
Date thebirthdate; // implementation detail
Address theaddress; // implementation detail
};

Here, if you do not access the class used by the person implementation, that is, the definition of string, date, and address, the class person cannot be compiled. This definition is generally provided through the # include command. Therefore, you may find something similar to this in the file defining the person class:

# Include <string>
# Include "date. H"
# Include "address. H"

Unfortunately, the compilation dependency between the file defining person and these header files is established. If some of these header files have changed or the files on which these header files depend have changed, files containing the person class must be re-compiled like those using the person class, this cascade compilation dependency brings countless troubles to the project.

You may want to know why C ++ insists on placing the implementation details of a class in the class definition. For example, why can't you define person in this way and specify the implementation details of this class separately?

Namespace STD {
Class string; // Forward Declaration (an incorrect
} // One-see below)

Class date; // Forward Declaration
Class address; // Forward Declaration

Class person {
Public:
Person (const STD: string & name, const Date & birthday, const address & ADDR );
STD: string name () const;
STD: String birthdate () const;
STD: String address () const;
...
};

If this is feasible, the customer of person must re-compile the class only when the class interface changes.

This idea has two problems. First, string is not a class, it is a typedef (for basic_string <char> ). The result is that the string Forward Declaration (Forward Declaration) is incorrect. Correct Forward Declaration is much more complicated because it includes another template. However, this is not important because you should not try to manually declare the parts of the standard library. As an alternative, directly use the appropriate # pair des and let it do it. Standard Header files are unlikely to become compilation bottlenecks, especially when your build environment allows you to use pre-compiled header files. If parsing standard header files is really a problem. You may need to change the design of your interface to avoid the use of the # program des standard library components that are undesirable.

The second (and more important) difficulty is that everything declared in the forward must let the compiler know the size of its object during compilation. Consider:

Int main ()
{
Int X; // define an int

Person P (Params); // define a person
...
}

When the compiler sees the definition of X, they know that they must allocate enough space (usually on the stack) to save an int ). This is fine. Every compiler knows how big an int is. When the compiler sees the definition of P, they know that they must allocate enough space for a person, but how can they infer how big a person object is? The only way they get this information is to refer to the definition of this class. But if a class definition that ignores the implementation details is legal, how does the compiler know how much space to allocate? This problem does not occur in languages such as Smalltalk and Java, because in these languages, when a class is defined, the compiler only allocates sufficient space for a pointer to an object. That is to say, they process the above Code and write it like this:

Int main ()
{
Int X; // define an int

Person * P; // define a pointer to a person
...
}

Of course, this is a legal C ++, so you can also play this kind of game of "hiding the implementation of classes behind a pointer" by yourself. One way to do this for person is to separate it into two classes. One provides only one interface and the other implements this interface. If the Implementation class is personimpl, the person can be defined as follows:

# Include <string> // standard library components
// Shouldn't be forward-declared

# Include <memory> // For tr1: shared_ptr; see below

Class personimpl; // forward Decl of person impl. Class
Class date; // forward decls of classes used in

Class address; // person Interface
Class person {
Public:
Person (const STD: string & name, const Date & birthday, const address & ADDR );
STD: string name () const;
STD: String birthdate () const;
STD: String address () const;
...

PRIVATE: // PTR to implementation;
STD: tr1: shared_ptr <personimpl> pimpl;
}; // STD: tr1: shared_ptr

In this way, the main class (person) does not contain any data member except a pointer pointing to its implementation class (personimpl) (here it is a tr1: shared_ptr -- See item 13. Such a design is often said to use pimpl idioms (pointing to the implementation Pointer "pointer to Implementation "). In such a class, the pointer name is often pimpl, just like the one above.

With this design, the customer of person is separated from the details of dates, addresses and persons. The implementation of these classes can be changed at will, but the customer of person does not have to re-compile. In addition, because they do not see the Implementation Details of person, the customer is unlikely to write code that depends on those details in some way. This is the real separation between interfaces and implementations.

The key to this separation is to replace the dependency on the definition with the dependency on the Declaration. This is the essence of minimizing compilation dependencies: as long as it can be implemented, it makes your header files independent and self-contained. If not, it depends on declarations in other files, rather than definitions. Everything else comes from this simple design strategy. Therefore:

When the object reference and pointer can be done, avoid using the object. You can define a reference or pointer to a type declaration. Objects defining a type must have a definition of this type.

As long as you can, use the dependency on the class declaration to replace the dependency on the class definition. Note that you do not need to define a class when declaring a function using a class, even if this function is passed or returned by passing values:

Class date; // class declaration
Date today (); // fine-no definition
Void clearappointments (date D); // of date is needed

Of course, passing a value is usually not a good idea, but if you find that you use it for some reason, you still cannot justify the introduction of unnecessary compilation dependencies.

Without declaring date, you can declare the capabilities of today and clearappointments, which may surprise you, but it is not as unusual as it looks. If someone calls these functions, the date definition must be seen before the call. Why bother declaring a function that nobody calls? Very simple. Not everyone calls them, but not everyone calls them. If you have a library that contains a lot of function declarations, it is unlikely that every customer needs to call every function. By transferring the responsibility for providing class definitions from the header file of your declared function to the file containing the function call, you eliminate the type dependency that the customer does not really need.

Provide header files for the declaration and definition respectively. To facilitate adherence to the preceding guidelines, the header file must appear in pairs: one for declaration and the other for definition. Of course, these files must be consistent. If a declaration is changed in one place, it must be changed in both places. The result is that the database client should always include a declaration file, instead of declaring something in front of itself, and the database author should provide two header files. For example, the customer who wants to declare the date of today and clearappointments should not manually declare the date in the forward direction as shown above. More appropriately, it should # include the appropriate header file for declaration:

# Include "datefwd. H" // header file declaring (but not
// Defining) class date

Date today (); // as before
Void clearappointments (date D );

Only the declared header file name "datefwd. H" is based on the header file from the Standard C ++ library <iosfwd>. <Iosfwd> contains the declaration of iostream components, and their definitions are defined in several different header files, including <sstream>, <streambuf>, <fstream>, and <iostream>.

<Iosfwd> it is also instructive in other aspects and explains that the suggestions proposed in this article are equally effective for templates and non-templates. In many build environments, the typical features of template definition are in header files, but some environments allow template definition in non-header files, therefore, it makes sense to provide a declarative header file for the template. <Iosfwd> is such a header file.

C ++ also provides the export keyword for separating template declarations from template definitions. Unfortunately, there are very few compilers that support export, and there is less practical experience dealing with export. As a result, it is too early to say what role export plays in efficient C ++ programming.

Classes such as person that use pimpl are often called handle classes. To avoid your curiosity about what such a class actually does, one way is to forward all calls to their functions to the corresponding implementation class, and use the implementation class for real work. For example, this is an example of how two person member functions can be implemented:

# Include "person. H" // We're re implementing the person class,
// So we must # include its class definition

# Include "personimpl. H" // we must also # include personimpl's class
// Definition, otherwise we couldn't call
// Its member functions; note that
// Personimpl has exactly the same
// Member functions as person-their
// Interfaces are identical

Person: Person (const STD: string & name, const Date & birthday,
Const address & ADDR)
: Pimpl (New personimpl (name, birthday, ADDR ))
{}

STD: String person: Name () const
{
Return pimpl-> name ();
}

Note how the person member function calls the personimpl member function and how the person: Name calls the personimpl: Name. This is important. To make a person a handle class, you don't need to change the person's way of doing things.

Another candidate method different from the handle class is to make person a special abstract base class called interface class. Such a class is used to specify an interface for the derived class. As a result, it is typically characterized by no data member and no constructor. There is a virtual destructor and a set of pure virtual functions for the specified interface.

The interface class is similar to interfaces in Java and. net, but C ++ does not impose constraints on interfaces in Java and. net for the interface class. For example, both Java and. Net do not allow data members and function implementations in interfaces, but C ++ does not prohibit such operations. The great elasticity of C ++ is useful. The implementation of non-virtual functions in all classes of an inheritance system should be the same, so it makes sense to implement such functions as part of the interface classes that declare them.

The interface class of a person may be like this:

Class person {
Public:
Virtual ~ Person ();

Virtual STD: string name () const = 0;
Virtual STD: String birthdate () const = 0;
Virtual STD: String address () const = 0;
...
};

Users of this class must program the pointer or reference to person, because it is impossible to instantiate a class containing pure virtual functions. (However, it is possible to instantiate a class derived from person.) Like a handle class customer, the interface class customers do not need to re-compile unless the interface of the interface class changes.

A customer of an interface class must have a way to create a new object. They generally do this by calling a function that assumes the role of the constructor as a derived class that can be practically instantiated. Such a function is generally called a factory function or virtual constructors ). They return pointers to dynamically allocated objects that support interfaces of the Interface Class (smart pointers are more suitable ). Such a function is generally declared as static within the interface class:

Class person {
Public:
...

Static STD: tr1: shared_ptr <person> // return a tr1: shared_ptr to a new
Create (const STD: string & name, // person initialized with
Const Date & birthday, // given Params; see item 18
Const address & ADDR); // Why a tr1: shared_ptr is returned
...
};

Customers use them like this:

STD: string name;
Date dateofbirth;
Address;
...

// Create an object supporting the person Interface
STD: tr1: shared_ptr <person> PP (person: Create (name, dateofbirth, address ));

...

STD: cout <PP-> name () // use the object via
<"Was born on" // person Interface
<PP-> birthdate ()
<"And now lives"
<PP-> address ();
... // The object is automatically
// Deleted when PP goes out

Of course, in some places, you must define a specific class that supports the interface class and call a real constructor. This is where everything happens after the virtual constructor is implemented in that file. For example, the interface class person can have a specific derived class realperson that provides the implementation of the virtual functions it inherits:

Class realperson: public person {
Public:
Realperson (const STD: string & name, const Date & birthday, const address & ADDR)
: Thename (name), thebirthdate (birthday), theaddress (ADDR ){}

Virtual ~ Realperson (){}

STD: string name () const; // implementations of these
STD: String birthdate () const; // functions are not shown,
STD: String address () const; // they are easy to imagine

PRIVATE:
STD: String thename;
Date thebirthdate;
Address theaddress;
};

For this specific realperson, writing person: Create does not have much value:

STD: tr1: shared_ptr <person> person: Create (const STD: string & name,
Const Date & birthday,
Const address & ADDR)
{
Return STD: tr1: shared_ptr <person> (New realperson (name, birthday, ADDR ));
}

Person: a more realistic implementation of create will create objects of different derived types, depending on, for example, parameter values of other functions, data read from files or databases, environment variables, and so on.

Realperson demonstrates one of the two most common interfaces for implementing an interface class mechanism: inherits its interface specification from the interface class (person), and then implements functions in the interface. The second method implementing an interface class contains multiple inheritance, which is discussed in item 40.

The handle class and the interface class separate the interfaces from the implementation, thus reducing the compilation dependency between files. If you are a preference, I know that you are looking for a limit on writing a small font. "What will all these tricks cheat me ?" You whispered. The answer is very common in Computer Science: it consumes some runtime speed, plus some extra memory for each object.

In the case of handle class, the member function must obtain the object data through the implemented pointer. This adds an indirect layer for each access. In addition, you must increase the pointer size of this implementation in the amount of memory required to store each object. Finally, the pointer to this implementation must be initialized (in the handle class constructor) to point to a dynamically allocated implementation object, therefore, you need to bear the inherent costs of dynamic memory allocation (and subsequent releases) and the possibility of encountering bad_alloc (out-of-memory) exceptions.

For the interface class, every function call is virtual, so you have to pay an indirect jump cost every time you call the callback function. Also, objects derived from interfaces must contain a virtual table pointer. This pointer may increase the amount of memory required to store an object, depending on whether the interface class is the only source of the virtual function of this object.

Finally, both the handle class and the interface class cannot be used in large quantities outside the inline function. Generally, the function ontology must be in the header file to achieve inline, but the handle class and interface class are generally designed to hide implementation details such as the function ontology.

However, simply giving up the handle class and interface class is a serious error because of the costs involved. The same is true for virtual functions, but you still cannot give up on them. Can you? (If you can, you have read the wrong book .) As an alternative, consider using these technologies in an improved way. In the development process, handle and interface classes are used to minimize the impact on customers when changes occur. When we can see that the difference in speed and/or size is sufficient to prove that the coupling between classes is worthwhile, we can replace the handle class and interface class with a specific class for product use.

Things to remember

The general idea behind minimizing compilation dependencies is to replace the dependencies on definitions with declarations. Two methods based on this idea are handle class and interface class.

The library header file should exist in a complete and only declared form. This applies whether or not a template is included.

 

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.