搜尋

首頁  >  問答  >  主體

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

RT

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

PHPzPHPz2804 天前577

全部回覆(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. 存取類別的成員函數或物件。

    例如,我現在開發一個功能fu​​nc,其實作要呼叫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
  • 取消回覆