Reading Notes Objective c ++ Item 31 minimizes the compilation dependencies between files.

Source: Internet
Author: User

Reading Notes Objective c ++ Item 31 minimizes the compilation dependencies between files.
1. Pull the whole body

Now you start to enter your C ++ program, and you have made a small change to your class implementation. Note that it is not an interface, but an implementation and a private part. Then you need to rebuild your program. It should take several seconds to calculate this build. After all, only one class is modified. When you click build or input make (or another method), you are shocked and ashamed, because you realize that the whole world has been recompiled and relinked! Do you feel resentful when this happens?

2. How does compilation dependency occur?

The problem is that C ++ is not good at separating interfaces from implementations. The class definition not only specifies the class interface but also specifies the details of many classes. For example:

 1 class Person { 2 public: 3 Person(const std::string& name, const Date& birthday, 4 const Address& addr); 5 std::string name() const; 6 std::string birthDate() const; 7 std::string address() const; 8 ... 9 private:10 std::string theName; // implementation detail11 Date theBirthDate; // implementation detail12 13 Address theAddress;              // implementation detail14 15 };

 

 

Here, the implementation of class Person requires some class definitions, that is, string, Date, and Address. If the class Person has no access to these class definitions, the Person will not be compiled. These definitions are provided using the # include command. Therefore, you may find code like the following in the file defining the Person class:

1 #include <string>2 3 #include "date.h"4 5 #include "address.h"

 

Unfortunately, a compilation dependency is established between the file defining the Person class and the header file listed above. Any header file is modified, or the files on which these header files depend are modified. files containing the Person class must be re-compiled, and any files using the Person class must be re-compiled. Such cascade compilation dependency will cause endless pain to a project.

 

3. Try to separate the implementation of the class

You may want to know why C ++ insists on putting the implementation details of the class into the class definition. For example, why can't you define the Person class so that the implementation details of the specified class are separated separately.

 1 namespace std { 2 class string; // forward declaration (an incorrect 3 } // one — see below) 4 class Date; // forward declaration 5 class Address; // forward declaration 6 class Person { 7 public: 8 Person(const std::string& name, const Date& birthday, 9 const Address& addr);10 std::string name() const;11 std::string birthDate() const;12 std::string address() const;13 ...14 };

 

If this is possible, the Person user must re-compile the class interface only when it is modified.

 

There are two problems with this idea. First, string is not a class, it is a typedef (basic_string <char> typedef ). Therefore, the pre-declaration on string is incorrect. The appropriate pre-declaration is actually more complex because it involves additional templates. However, this does not matter, because you should not try to manually declare some parts of the standard library. On the contrary, it is easy to use the appropriate # include to achieve the goal. The standard header file does not look like a compilation bottleneck. In particular, your compiling environment allows you to use the pre-compiled header file. If parsing standard header files is really a problem, you may need to modify your interface design to avoid using certain parts of the standard library (using some parts of the standard library needs to be unpopular # Using DES ).

 

The second difficulty (and more obvious) in pre-declaration of each thing is that the compiler needs to know the object size during compilation. Consider:

1 int main()2 {3 int x;                      // define an int4 5 Person p( params ); // define a Person6 7 8 ...9 }

 

When the compiler sees the definition of x, it knows that it must allocate enough space for an int. This is okay. The compiler knows the size of an int. When the compiler sees the definition of p, they know that they must allocate enough space for a Person, But how do they know the size of a Person object? The only method is to view the class definition. But for a class definition, it is legal to ignore the implementation details. How does the compiler know how much space to allocate?

This problem does not occur in languages like Smalltalk and Java, because when an object is defined in these languages, the compiler only allocates enough space for pointers to the object. For the above Code, they will be processed as follows:

1 int main()2 {3 int x;         // define an int4 5 Person *p; // define a pointer to a Person6 ...7 }

 

This is of course Legal C ++ code, so you can play the game "Hide object details behind Pointers" by yourself. For Person, one implementation method is to divide it into two classes. One provides only interfaces and the other implements interfaces. If the Implementation class is named PersonImpl, Person is defined as follows:

 1 #include <string> // standard library components 2 // shouldn’t be forward-declared 3  4 #include <memory> // for tr1::shared_ptr; see below 5  6 class PersonImpl; // forward decl of Person impl. class 7  8 class Date;             // forward decls of classes used in 9 10  11 12 class Address;                                                                      // Person interface13 14 class Person {                                                                      15 16 public:                                                                                17 18 Person(const std::string& name, const Date& birthday,       19 20 const Address& addr);                                                        21 22 std::string name() const;                                                    23 24 std::string birthDate() const;                                              25 26 std::string address() const;                                                 27 28 ...                                                                                        29 30 private:                                                                               // ptr to implementation;31 32 33 std::tr1::shared_ptr<PersonImpl> pImpl; // see Item 13 for info on34 }; // std::tr1::shared_ptr

 

Here, the main class (Person) does not contain any data members, only contains the pointer to the class implementation (PersonImpl), a tr1: shared_ptr pointer (Item 13 ). This design is commonly referred to as "pimpl idiom" (pointing to the implementation pointer ). In such a class, the pointer name is usually pImpl, as shown above.

With this design, the Person user is separated from the Implementation Details of datas, address, and persons. The implementation of these classes can be modified at will, but the Person user does not need to re-compile. In addition, because they cannot see the Implementation Details of Person, users should not write code that depends on these details. This truly separates the implementation from the interface.

4. Design Strategy for minimizing compilation Dependencies

The key to separation is to replace the dependency on the definition with the dependency on the Declaration. This is the essence of minimizing compilation dependencies:In practice, you can make your header files self-sufficient. If you do not meet this requirement, You need to rely on declarations in other files instead of definitions.. Other designs come from this simple design strategy. Therefore:

 

  • Do not use objects when you can use references and pointers to objects. You can use only one declaration to define references and pointers pointing to a type. To define a type object, you must use the class definition.
  • Replace the class definition with the class Declaration whenever possible. Note that when you use a class to declare a function, you will never use the definition of this class. You do not even need to use a value to pass a parameter or return a value:

 

1 class Date; // class declaration2 3 Date today();     // fine — no definition4 5    id clearAppointments(Date d); // of Date is needed

 

 

Of course, passing by value is usually a bad method (Item 20), but if you find that you need to use it for some reason, there is no reason to introduce unnecessary compilation dependencies.

 

It may surprise you that you do not need to define Date when declaring today and clearAppointments, but it is not as curious as it looks. If anyone calls these functions, the Data definition must be seen before the function call. Why do you declare a function that is not called? Very simple. No one will call them, but not everyone will call them. If you have a library containing many function declarations, it is unlikely that every user can call each function. By transferring the responsibility for providing class definitions in the header file of declared functions to the client file containing function calls, you eliminate unnecessary manual user dependencies on Type Definitions.

 

  • Provide header files for declarations and definitions

 

To comply with the preceding guidelines, the header file must be used in pairs: one for declaration and the other for definition. Of course, these files should be consistent. If the declaration of one place is modified, both places must be modified at the same time. Finally, the database user should always # include a declaration file, instead of declaring it in front of it ,. For example, if you want to declare the today and clearAppointments of the Date class, you do not need to declare the Date in the forward direction as above. Instead, we should # include the declared header file:

1 #include "datefwd.h" // header file declaring (but not2 3 // defining) class Date4 5 Date today(); // as before6 7 void clearAppointments(Date d);

 

The header file "datefwd. h" only contains the Declaration. It is named based on the Standard C ++ library header <iosfwd> (Item 54 ). <Iosfwd> contains the declaration of the iostream component. Definitions corresponding to these declarations are placed in several different headers, including <sstream>, <streambuf>, <fstream> and <iostream>.

<Iosfwd> another guiding significance is to clarify that the suggestions in this clause apply not only to templates, but also to non-templates. Although Item30 explains in many compiling environments, the template definition is usually placed in the header file, and some compiling environments allow the template definition to be placed in non-header files, therefore, it makes sense to provide a template with a header containing only declarations. <Iosfwd> is such a header.

 

The export keyword is also provided in C ++, which can separate the template declaration from the template definition. No, there are few compilers that support export. In the real world, there is also little experience in using export. Therefore

It is too early for export to play a role in efficient C ++ programming .,

 

5. Handle class

Classes like Person that use pimpl idiom are usually called handle classes. If you want to know how such classes are omnipotent, one way is to transfer all function calls to the corresponding implementation class, and the real work is carried out in the implementation class. For example, the following shows how two member functions of the Person class are implemented:

 1 #include "Person.h" // we’re implementing the Person class, 2  3 // so we must #include its class definition 4  5 #include "PersonImpl.h" // we must also #include PersonImpl’s class 6  7 // definition, otherwise we couldn’t call 8  9 // its member functions; note that10 11 // PersonImpl has exactly the same public12 13 // member functions as Person — their14 15 // interfaces are identical16 17 Person::Person(const std::string& name, const Date& birthday,18 19 const Address& addr)20 21 : pImpl(new PersonImpl(name, birthday, addr))22 23 {}24 25 std::string Person::name() const26 27 {28 29 return pImpl->name();30 31 }

 

 

Note how the Person constructor calls the PersonImpl Constructor (by using new Item 16) and how the Person: name calls the PersonImpl: name. Defining the Person class as a handle class does not change what the Person class can do, but only modifies the implementation method of the Person class.

6. abstract base class

Another alternative to the handle class is to define Person as a special abstract base class, that is, an interface class. The intention of using this type is to specify an interface (Item 34) for the derived class ). This type has no data members, no constructor, and there is a virtual destructor (item7) and a series of pure virtual functions.

Interfaces are similar to interfaces in Java and. NET. However, Java and. NET impose restrictions on interfaces, but c ++ does not. For example, neither Java nor. NET can declare data members or implement functions in interfaces, but C ++ has no restrictions on both. This higher flexibility of C ++ is useful. Item36 explains that in an inheritance system, the same non-virtual functions should be implemented for all classes. Therefore, for functions declared in the interface class, it makes sense to implement it as part of an interface class.

 

The implementation of a Person class interface may look like the following:

 1 class Person { 2  3 public: 4  5 virtual ~Person(); 6  7 virtual std::string name() const = 0; 8  9 virtual std::string birthDate() const = 0;10 11 virtual std::string address() const = 0;12 13 ...14 15 };

 

Users of this class must rely on the Person pointer or reference for programming, because it is impossible to instantiate a class that contains pure virtual functions. (However, it is possible to instantiate the derived class of Person ). Just like a user of the handle class, the interface class must be re-compiled only when its interface changes. It is not required in other cases.

 

A user of an interface class must have a method to create a new object. Generally, this is achieved by calling the function that assumes the role of the constructor of the derived class. Of course, the derived class can be instantiated. Such functions are usually called factory functions (Item13) or virtual constructors ). They return pointers to dynamically allocated objects (Item 18 is better with smart pointers ). Such functions are usually declared as static in the interface class:

 1 class Person { 2  3 public: 4  5 ... 6  7 static std::tr1::shared_ptr<Person> // return a tr1::shared_ptr to a new 8  9 create(const std::string& name, // Person initialized with the10 11 const Date& birthday, // given params; see Item18 for12 13 const Address& addr); // why a tr1::shared_ptr is returned14 15 ...16 17 };

 

The user uses it as follows:

 1 std::string name; 2  3 Date dateOfBirth; 4  5 Address address; 6  7 ... 8  9 // create an object supporting the Person interface10 11 std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));12 13 ...14 15 std::cout << pp->name() // use the object via the16 17 << " was born on " // Person interface18 19 << pp->birthDate()20 21 << " and now lives at "22 23 << pp->address();24 25 ... // the object is automatically26 27 // deleted when pp goes out of28 29 // scope — see Item13

 

Of course, you must define the existing classes that support interface-class interfaces and call the real constructor in the current class. This occurs in files that contain the Implementation of Virtual constructor. For example, the Person interface class may have a freshly derived class RealPerson, which provides implementation for virtual functions inherited from the base class:

 1 class RealPerson: public Person { 2  3 public: 4  5 RealPerson(const std::string& name, const Date& birthday, 6  7 const Address& addr) 8  9 :  theName(name), theBirthDate(birthday), theAddress(addr)10 11 {}12 13 virtual ~RealPerson() {}14 15 std::string name() const; // implementations of these16 17 std::string birthDate() const; // functions are not shown, but18 19 std::string address() const; // they are easy to imagine20 21 private:22 23 std::string theName;24 25 Date theBirthDate;26 27 Address theAddress;28 29 };

 

Given the definition of RealPerson, implementing Person: create becomes insignificant:

 1 std::tr1::shared_ptr<Person> Person::create(const std::string& name, 2  3 const Date& birthday, 4  5 const Address& addr) 6  7 { 8  9 return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday,10 11 addr));12 13 }

 

Person: a more practical implementation of create is to create different derived class objects. The object type may depend on additional function parameters, data or environment variables read from files or databases.

 

There are two common mechanisms for implementing an interface class. RealPerson shows one of them: its interface inherits from the interface class (Person), and then implements functions in the interface. The second method for implementing the interface class involves multi-inheritance, which will be involved in Item40.

 

 

7. Additional overhead is required to use the interface class and handle class

The handle class and Interface Class decouple the interface from the implementation, thus reducing the compilation dependency between files. You may ask, what does this trick make me pay? The answer is also a common answer in Computer Science: It slows down the running speed,

In addition, additional space is allocated for each object.

 

In the example of a handle class, a member function must point to the implemented pointer to reach the object data. This adds an indirect layer for each access. You must add the size of the implementation pointer to the memory capacity required to store each object. Finally, the implementation pointer must be initialized as an implementation object pointing to dynamic allocation. Therefore, you introduce the inherent overhead of dynamic memory allocation (and subsequent Memory destruction, it is also possible to encounter a bad_alloc (memory overflow) exception.

 

For interface classes, every function call is virtual, so every time you call a function, there will be an indirect jump overhead (Item 7 ). At the same time, the object derived from the interface class must contain a virtual table pointer (Item7 ). This pointer may increase the memory capacity required by OSS, depending on whether the interface class provides the unique source of the virtual function for this object.

 

Finally, the handling class and interface class of the inline function won't do much. Item30 explains why the function body must be placed in the header file to be inline, but the handle and interface class are designed to hide implementation details like the function body.

 

If you discard the handle class and interface class because of the extra overhead, it is a big mistake. This is also true for virtual functions. You don't want to give up on them, do you? (If you want to give up, the books you read are wrong.) on the contrary, you can use them in a step-by-step manner. In the development process, use the interface class and handle class to minimize the impact of modification on users. If the effect of the handle class and interface class on speed and capacity is greater than the coupling between classes, replace the current class.

 

8. Summary:

The idea behind minimizing compilation dependencies is to rely on declarations rather than definitions. The method based on this idea is the handle class and interface class.

The library header file should exist in a completely and only declarative manner. Templates are applicable regardless of whether they are involved.

Related Article

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.