search

Home  >  Q&A  >  body text

编译器 - 一个C++工程中,许多个文件都include某一个类,当该类更新时,编译速度太慢,怎么办?

RT

关于类的设计 还真是个考验啊

PHPzPHPz2804 days ago578

reply all(4)I'll reply

  • 迷茫

    迷茫2017-04-17 11:45:23

    Oh, that’s a good question, and although it’s a cliché, very few people actually know the solution. "Effective C++" has an introduction, and I recommend this book to all C++ers. I will briefly summarize it here.

    In a large-scale project with organizational problems, the biggest problem affecting the compilation speed is that the header files form a huge dependency network. Modification of one of the header files will cause a large number of indirectly dependent source code files to be recompiled. a.h contains b.h, b.h contains c.h, and c.h contains d.h. Even if a.h and d.h seem to have nothing to do, it is inevitable that a.cc will be recompiled when you modify d.h.

    First of all, you need to know a feature of C++. Everyone knows that functions are divided into two parts: declaration and implementation, but classes can also be divided into pre-declaration and definition. Maybe fewer people know it, and even more people know how to use it. If it is less, it can actually be used to solve the compilation speed problem.

    class Object;//前置声明
    
    class Object { //类定义
        void Method();
    };
    
    void Object::Method() { //实现
    }
    

    These three parts can be written in three files, namely "filefwd.h" "file.h" "file.cc", the latter one includes the previous one. Among them, filefwd.h is called a "predeclaration file", and many open source projects are designed this way.

    "Effective C++" summarizes that in many cases you do not need to include "file.h", you only need to include the forward declaration "filefwd.h":

    1. Use a reference or pointer to a class, including as a smart pointer.
    2. Use classes as return types.

    You must include "file.h" in the following cases:

    1. Use classes as member objects.
    2. Use classes as base classes.
    3. Use classes as function parameters.
    4. Access member functions or objects of a class.

    For example, I am now developing a function func whose implementation calls the Method of Object. I can write like this:

    func.h:

    #ifndef INCLUDE_FUNC_H
    #define INCLUDE_FUNC_H
    
    #include "filefwd.h"
    void DoSomething(Object& obj); //使用Object的引用,无需包含file.h
    
    #endif
    

    func.cc:

    #include "func.h"
    #include "file.h"
    void DoSomething(Object& obj) {
        obj.Method(); //调用Object的成员函数,必须包含file.h
    }
    

    What’s the use of all this trouble? You will find that most .h files do not need to contain other .h files, but only need to contain fwd.h. Since fwd.h does not need to define members of the class, it has very few dependencies and rarely contains other files. And only .cc needs to contain a lot of .h.

    So your code forms a concise header file dependency hierarchy, .cc depends on *.h, .h depends on fwd.h, instead of a lot of .h relying on each other, each modification of .h requires recompiling There are very few .cc files and the compilation speed is faster.

    In addition, there is another technique that should be used in combination, which is the Impl design pattern mentioned by @spacewander. Its purpose is to move more dependencies to .cc files and reduce .h dependencies as much as possible. For example, if I want to design a class Object, I need to use vector as the internal implementation. Using impl can avoid including vector in .h.

    filefwd.h The pre-declaration file mentioned earlier:

    #ifndef INCLUDE_FILE_FWD_H
    #define INCLUDE_FILE_FWD_H
    
    class Object;
    
    #endif
    

    file.h:

    #ifndef INCLUDE_FILE_H
    #define INCLUDE_FILE_H
    
    #include "filefwd.h"
    class Object { //一个包装的类,只有成员函数和一个指针
    public:
        Object();
        ~Object();
        void Method();
    private:
        void* impl;
    };
    
    #endif
    

    file.cc:

    #include "file.h"
    #include <vector> //vector无需写在file.h中
    
    class ObjectImpl { //一个被隐藏的实现类,就是上面指针指向的对象,所有成员对象和实现都写在这里。
    public:
        void ObjectImpl() {...}
        void ~ObjectImpl() {...}
        void Method() {...} //逻辑都在这里实现
    private:
        std::vector<int> vec;
    };
    
    void Object::Object()
        :impl(new ObjectImpl()) //构造时new一个实现类
    { } 
    void Object::~Object() {
        delete (ObjectImpl*)impl; //析构时delete掉
    } 
    void Object::Method() {
        ((ObjectImpl*)impl)->Method(); //简单地把任务转给隐藏实现类。
    }
    

    The combination of the two modes not only forms an elegant "separation of interface and implementation", but also makes it so refreshing during compilation. There is no need to worry about the additional runtime overhead caused by one more call. In fact, as long as all methods of the impl class are inlined (even the compiler will automatically add them), there will be no performance loss at all. The cost of this is that each module has a lot of interface code (look at the example above, almost twice as much), even more than the logic itself, and there are trade-offs before using it. My experience is: it’s worth it to make your code look more stylish and be admired by other programmers!

    reply
    0
  • 巴扎黑

    巴扎黑2017-04-17 11:45:23

    You can consider the Impl idiom in C++.
    Put the function into an Impl class, and then this class holds a pointer to the Impl class, and the external interface is implemented by calling the corresponding method of the Impl class.

    //Stack.h
    class Stack{
    public:
        Stack();
        ~Stack();
    public:
        void push(int i);
        int pop();
    private:
        class StackImpl;//StackImpl类声明
        StackImpl *pStackImpl;
    }
    
    // StackImpl.h
    class StackImpl{
    public:
        StackImpl();
        ~StackImpl();
    public:
        void push(int i);
        int pop();
    }
    
    // Stack.cpp
    #include "StackImpl.h"
    
    void Stack::push(int i)
    {
        pStackImpl->push(i);
    }
    ...
    

    reply
    0
  • 阿神

    阿神2017-04-17 11:45:23

    For this kind of class that is referenced by multiple classes, the stability of the interface should be ensured. That is, the implementation in the .cpp file can be greatly changed, but the .h file must remain stable, which requires the initial design of the class. You need to be thoughtful and flexible at all times, fully consider the functions that may need to be added later, design the interface first, and then implement them one by one. If you do not need it temporarily, you can reserve the interface without implementing it. If the initial consideration is not enough, during the project progress, when the amount of code is not so large that it is difficult to maintain, continue to refactor until the interface design of each commonly used class becomes reasonable and stable, and then expand on this basis. This is true for anything as small as a class, as large as a module, a component, or even an entire project. It is necessary to refactor the code as much as possible before it becomes too complex to maintain, and then expand it until the interface becomes reasonable and stable.
    But after saying so much, it doesn’t seem to be of much help to your problem haha.

    reply
    0
  • 迷茫

    迷茫2017-04-17 11:45:23

    In addition to keeping the interface stable, precompiling header files into gch files or pch files can also help speed up compilation.

    For specific information, please refer to Wikipedia: https://en.wikipedia.org/wiki/Precompiled_header

    reply
    0
  • Cancelreply