Reactive C ++: Clause 31: minimizes the compilation dependency between files. Reactive dependency

Source: Internet
Author: User

Reactive C ++: Clause 31: minimizes the compilation dependency between files. Reactive dependency

    If you are modifying the program, you only modify the implementation of a class interface and modify the private part. After that, you will find that many files have been re-compiled. This problem occurs in the absence of "Separating interfaces from implementations ". The definition of Class not only describes the class interface in detail, but also includes many implementation details:

    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: // Implementation Details std: string theName; Date the BirthDate; Address theAddress ;};

    To compile, you must include the string, Date, and Address used in the class. InPersonThe front of the definition file should include:

        #include<string>    #include"date.h"    #include"address.h"

    In this way,PersonA compilation dependency is formed between the definition file and Its included files ). If these header files have a modification, the files using Person class must be re-compiled. Such dependencies will cause a lot of inconvenience to the project.
    So why does C ++ place the implementation details of the class in the class definition form? The implementation details can be separated:

    Namespace std {class string; // pre-declaration} class Date; // pre-declaration class Address; // pre-declaration class Person {public :...... };

    First, we will not discuss whether the pre-declaration is correct (which is actually incorrect). If this can be done, the Person user only needs to re-compile it when the Person interface is modified. But there are two problems with this idea.
    -1. string is not a class. It is a typedef and defined as basic_string. The preceding statement is incorrect for the former statement of string, and the correct pre-statement is complicated because it involves additional templates. In fact, we should not declare the standard library. Use # include. Standard Header files generally do not become compilation bottlenecks, especially when precompiled headers are allowed in your build environment ). If parsing (parsing) standard header files is a problem, you generally need to modify your interface design.
    -2. The compiler must know the object size during compilation for everything declared by the prefix. For example

     int main()    {        int x;        Person p( params);        ……    }

    When the compiler sees the x definition, it must know how much memory is allocated to x; then, when the compiler sees the p definition, it should also know how much memory is allocated to p. If the class definition does not list implementation details, the compiler cannot know how much space is allocated to p.

    This problem does not exist in languages such as Java, because when defining objects, the compiler only assigns a pointer (to point to this object ). The above code implementation looks like this:

    Int main () {int x; Person * p; // defines a pointer to Person ...... }

    You can also do this in C ++ to hide the object implementation details behind a pointer. For Person, it can be divided into two classes, one is responsible for providing the interface, and the other is responsible for implementing this interface. The interface to be implemented is named PersonImpl (Person implementation ):

    # Include <string> # include <memory> class PersonImpl; // pre-declaration class Date; class Address; 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: // Implementation Details std: tr1: shared_ptr <PersonImpl> pImpl; // pointer, pointing to Implementation };

    This design is called pimpl idiom (pimpl: pointer to implementation ). The customer of Person is separated from the Implementation Details of Date, Address, and Person. Do not re-compile the client for any implementation changes of classes. In addition, the customer cannot see the implementation details of the Person, and will not write "depends on the specific code", truly implementing "interface and implementation separation ".

    The key to this separation is that the declaration dependency replaces the definition dependency, which is the essence of minimizing the compilation dependency: In reality, let the header file satisfy itself as much as possible, in case it cannot be done, so that it is dependent on the declarations (not definitions) in other files. All others are derived from the following design strategies:

    • If you can use object references or object pointers to complete the task, do not use the object. Because only one declaration is required to use references or pointers, and the definition of objects needs to use the definition of this type.
    • If possible, replace the class definition with the class declarative. However, when a declared function uses a class, class definition is not required even if this type of parameter/return value is passed by value. However, when using these functions, these classes must be exposed before calling the function. After all, the customer needs to know the definition of classes, but the significance of doing so is to provide the class definition (completed by # include) obligations, from the header file where the function declaration is located, transfers the client file to the function call.
    • Two different header files are provided for declarative and defined headers.. In the program, the customer should not be asked to give a pre-declaration. The program author generally provides two header files, one for declaration and the other for definition. In the header file of the C ++ standard library (clause 54 ),<iosfwd>Declarations containing iostream components are defined and distributed in different files, including<sstream>,<streambuf>,<fstream>,<iostream>.

    <iosfwd>Note: These terms also apply to templates and non-templates.TermsAs mentioned in article 30, the template is usually defined in the header file, but some build environments allow the template to be defined in non-header files. In this way, the header file containing only declarative files can be provided to the templates.<iosfwd>Is such a file.

    In C ++, the keyword export is provided to separate the template declaration and definition into different files. However, there are not many compilers that support the export keyword.

    The classes that use pimpl idiom like Person are called Handle classes. One of the ways in which such classes actually do things is to transfer all their functions to the corresponding implementation class (implementation classes), which completes the actual work. For example, the Person implementation:

        #include"Person.h"    #include"PersonImpl.h"    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();    }    ……

    In PersonImpl, there is a member function that is exactly the same as Person, and the two interfaces are exactly the same.

    Another way to implement Handle class is to make Person a special abstract base class (abstract base class) called Interface class. This class member variable only describes the derived classes interface (Terms34), no constructor, only one virtual destructor (Terms7) and this group of pure virtual functions are used to describe the entire interface.

    Interface classes is similar to interfaces of Java and. NET, but the Interface class of C ++ is different from interfaces in Java and. NET. It allows variables and is more flexible. AsTerms36. "non-virtual function implementation" should be the same for all the classes in the inheritance system. It is also reasonable to implement such a function as part of Interface class (where the corresponding declaration is written.

    Interface class like Person can be written as follows:

     class Person{    public:        virtual ~Person();        virtual std::string name() const=0;        virtual std::string birthDate()const=0;        virtual std::string address()const=0;        ……    };

    The customer of this class must use the Person's pointers or references, because the class containing the pure virtual function cannot be instantiated. In this way, as long as the Interface class Interface is not modified, other customers do not need to re-compile.

    When an Interface class user creates a new object for the class, a special function is usually used to assume the role of the derived classes constructor that "will be embodied. Such a function is called the engineering function factory (Clause 13) Or virtual constructor. They return pointers (more likely smart pointers, ** terms ** 18) and point to the dynamically allocated objects. This object supports the Interface class Interface. The factory function is usually declared as static.

     class Person{    public:        ……        static std::tr1::shared_ptr<Person create(……)        ……    };

    The customer uses this method.

        std::string name;    Date dateOfBirth;    Address address;    std::tr1::shared_ptr<Person> pp(Person::create(……));    std::cout<<pp->name()<<"was born"<<pp->birthDate()<<"and now lives at"<<pp->address();

    The specific class (concrete classes) that supports the Interface class Interface must be defined before calling the real constructor. For example, a RealPerson inherits the Person

        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;        ……    private:        std::string theName;        Date theBirthDate;        Address theAddress;    };

    With RealPerson, you can write "Person: create ".

     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));    }

    RealPerson implements Interface class by inheriting interfaces from Interface class and then implementing the functions covered by interfaces. There is also an implementation method to design multiple inheritance, which is discussed in ** clause ** 40.

    Handle classes and Interface classes remove the coupling between interfaces and implementations, thus reducing compilation dependency. But it also brought some cost: You lost some speed during the running, and opened up a number of memory beyond the object.

    In Handle classes, the member function obtains object data through implementation pointer. In this way, an indirect layer is added for access, and the memory size of implementation pointer is also increased. The initialization of implementation pointer also carries the additional overhead of dynamically opening up the memory, and the possibility of encountering the bad_alloc exception.

    In Interface classes, every function is virtual, so an indirect jump (indirect jump) is required for each call. The derived object has a vptr (virtual table pointer, ** terms ** 7), which increases the memory required for the object.

    Handle classes and Interface classes cannot be too large once they are separated from the inline function. ** Clause ** 30 explains why inline functions should be placed in header files, but Handle classes and Interface classes are designed to hide implementation details.

    What we need to do is to use Handle classes and Interface classes in the program to minimize the impact on customers when the code changes. However, if they cause excessive extra costs, such as the running speed or object size difference, so that the coupling between classes is not critical, the specific class (concrete classes) is used) replace Handle classes and Interface classes.

    Summary
    -The general idea of supporting "minimal compilation dependency" is: dependent on declarative, not dependent on definitions. The two methods based on this idea are Handle classes and Interface classes.
    -The library header file should exist in the form of "full and declaration-only forms. Whether or not these templates are used.

    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.