検索

ホームページ  >  に質問  >  本文

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

RT

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

PHPzPHPz2813日前586

全員に返信(4)返信します

  • 迷茫

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

    哦,这是个好问题,虽然老生常谈,但真正知道解决方案的人很少。《Effective C++》有介绍,同时推荐这本书给所有C++er。我这里就简单归纳一下。

    一个组织有问题的大型项目中,影响编译速度的最大问题就是头文件形成庞大的依赖网络,其中一个头文件修改就导致一大堆间接依赖的源代码文件需要重新编译。a.h包含b.h,b.h包含c.h,c.h又包含d.h,即使a.h和d.h似乎没什么关系,你修改d.h的时候还是无可避免a.cc被重新编译。

    首先得知道C++一个特性,函数分为声明和实现两部分是人所皆知,但类也可以分为前置声明和定义可能知道的人就比较少了,知道能怎么用就更少了,其实就是可以用来解决编译速度问题的。

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

    这三部分可以分在三个文件里写,分别是 "filefwd.h" "file.h" "file.cc",后面一个包含前面一个。其中filefwd.h叫做“前置声明文件”,很多开源项目就是这样设计的。

    《Effective C++》归纳了很多时候你不需要包含"file.h",只需要包含前置声明"filefwd.h":

    1. 使用类的引用或指针,包括作为智能指针时。
    2. 使用类作为返回类型。

    以下情况你必须包含"file.h":

    1. 使用类作为成员对象。
    2. 使用类作为基类。
    3. 使用类作为函数传值参数。
    4. 访问类的成员函数或对象。

    例如,我现在开发一个功能func,其实现要调用Object的Method,我可以这样写:

    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
    }
    

    这么折腾有什么用?你会发现绝大部分的.h文件都无需包含其它.h,而只需要包含fwd.h,而fwd.h由于不需要定义类的成员,所以依赖极少,很少包含别的文件。而只有.cc需要包含很多的.h。

    于是你的代码就形成了简洁的头文件依赖层次,.cc依赖*.h,.h依赖fwd.h,再不是一大堆.h互相依赖,每修改一个.h造成要重新编译的.cc很少,编译速度也更快。

    另外,还有一个要结合使用的技巧,就是 @spacewander 提到的Impl设计模式,其目的就是把更多的依赖往.cc文件移动,尽可能减小.h的依赖。例如,我要设计一个类class Object,需要使用vector作为内部实现,使用impl就可以避免在.h中包含vector。

    filefwd.h 前面说的前置声明文件:

    #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(); //简单地把任务转给隐藏实现类。
    }
    

    两种模式结合使用,不但形成优雅的“接口和实现分离”,编译时更是那个酸爽。不用担心多一次调用带来额外的运行期开销,实际上只要将impl类的所有方法加上inline(甚至编译器会自动加),性能完全没有损失。这样做的代价是每个模块都有大量的接口代码(看看上面的例子,几乎多了两倍),甚至比逻辑本身更多,使用前需要权衡。我的经验是:为了让你的代码看起来更有逼格和其他程序猿仰慕的目光,值了!

    返事
    0
  • 巴扎黑

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

    可以考虑下C++中的Impl惯用法。
    把功能放到一个Impl类中,然后这个类持有Impl类的指针,而对外的接口通过调用Impl类的对应方法来实现。

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

    返事
    0
  • 阿神

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

    对于这种被多个类引用的类,应当保证接口的稳定,也就是.cpp文件里的实现可以做大的改动,但是.h文件要保持稳定,这就要求最初设计该类的时候考虑周全,还要有弹性,充分考虑之后可能需要添加进来的功能,先设计接口,再逐一实现,暂时不需要的可以预留接口不做实现。如果最初考虑不足,要在项目进展过程中代码量尚且没有多到难以维护的时候不断进行重构,直至每个常用类的接口设计都趋于合理和稳定,然后在这个基础上再去扩展。小至一个类,大到一个模块,一个组件,乃至整个项目都是这样,要在代码复杂到难以维护之前尽可能重构,直至接口趋于合理稳定再进行扩展。
    不过说了这么多,好像对你这个问题也没什么帮助呵呵。

    返事
    0
  • 迷茫

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

    除了保持接口稳定以外,把头文件预编译成gch文件或者pch文件也对加快编译有一定的帮助。

    具体信息可以参考维基百科: https://en.wikipedia.org/wiki/Precompiled_header

    返事
    0
  • キャンセル返事