effective C++ 條款 31:將檔案間的編譯依存關係降至最低

來源:互聯網
上載者:User

假設你對c++程式的某個class實現檔案做了些輕微改變,修改的不是介面,而是實現,而且只改private成分。

然後重建立置這個程式,並預計只花數秒就好,當按下“Build”或鍵入make,會大吃一驚,因為你意識到整個世界都被重新編譯和連結了!

問題是在c++並沒有把“將介面從實現中分離”做得很好。class 的定義式不只詳細敘述了class介面,還包括十足的實現細目:

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:
    std::string theName;        //實現細目
    Date theBirthDate;          //實現細目
    Address theAddress;         //實現細目
};

這個class Person無法通過編譯,Person定義檔案的最上方可能存在這樣的東西:

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

這樣一來,便在Person定義檔案和其含入檔案之間形成了一種編譯依存關係(compilation dependency)。如果這些標頭檔中有任何一個被改變,或這些檔案所依賴的其他標頭檔有任何改變,那麼每個含入Person class的檔案就得重新編譯,任何使用Person class的檔案也必須重新編譯。這樣的的連串編譯依存關係(cascading compilation dependencies)會對許多項目造成難以形容的災難。

為什麼c++堅持將class的實現細目置於class定義式中?為什麼不這樣定義Person,將實現細目分開敘述:

namespace std { class string;} //前置聲明(不正確)
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;
    ...
};

如果這樣,Person的客戶就只有在Person介面被修改時才重新編譯。

兩個問題:第一,string不是個class,它是個typedef。因此string前置聲明並不正確,而且你本來就不應該嘗試手工聲明一部分標準程式庫。你應該僅僅使用適當的#includes完成目的。標準標頭檔不太可能成為編譯瓶頸,

第二,編譯器必須在編譯期間知道對象的大小:

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

編譯器知道必須分配足夠空間放置一個Person,但是他必須知道一個Person對象多大,獲得這一資訊的唯一辦法是詢問class定義式。然而,如果class定義式可以合法的不列出實現細目,編譯器如何知道該分配多少空間?

此問題在smalltalk,java等語言上並不存在,因為當我們以那種語言定義對象時,編譯器只分配足夠空間給一個指標(用於指向該對象)使用。就是說它們將上述代碼視同這樣子:

int main()
{
    int x;
    Person* p;
}

這當然也是合法的c++代碼,所以你可以玩玩“將對象實現細目隱藏在一個指標背後”的遊戲。可以把Person分割為兩個classes,一個提供介面,另一個負責實現介面。負責實現的那個所謂的implementation class取名為PersonImpl,Person將定義如下:

#include <string>
#include <memory>
class PersonImpl;
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:
    std::tr1::shared_ptr<PersonImpl> pImpl; //指向實現物的指標
};

這裡,Person只內含一個指標成員,指向其實作類別(PersonImpl)。這個設計常被稱為pimpl idiom(pimpl是“pointer to implementation”的縮寫)。

這樣,Person的客戶就完全與Date,Address以及Person的實現細目分離了。那些classes的任何實現修改都不需要Person用戶端重新編譯。

這個分離的關鍵在於以“聲明的依存性”替換“定義的依存性”,那正是編譯依存性最小化的本質:讓標頭檔儘可能自我滿足,萬一做不到,則讓它與其他檔案內的聲明式(而非定義式)相依。其他每件事都源自於這個簡單的涉及策略。

如果用object reference 或 object pointer可以完成任務,就不要用objects。

可以只靠聲明式定義出指向該類型的pointer和reference;但如果定義某類型的objects,就需要用到該類型的定義式。

如果能夠,盡量以class聲明式替換class定義式。

當你聲明一個函數而它用到某個class時,你並不需要該class的定義式,縱使函數以by value方式傳遞該類型的參數(或傳回值)亦然:

class Date; //class 聲明式
Date today();
void clearAppiontments(Date d);

聲明today函數和clearAppointments函數無需定義Date,但是一旦有任何人調用那些函數,調用之前Date定義式一定得先曝光才行。如果能夠將“提供class定義式”(通過#include完成)的義務從“函式宣告所在”之標頭檔移轉到“內含函數調用”之客戶檔案,便可將“並非真正必要之類型定義”與用戶端之間的編譯依存性去除掉。

為聲明式和定義式提供不同的標頭檔。

因此程式庫客戶應該總是#inlcude一個聲明檔案而非前置聲明若干函數,

#include "datefwd,h" //這個標頭檔內聲明class Date
Date today();
void clearAppointments(Date d);

只含聲明式的那個標頭檔名為“datefwd.h”,像標準程式庫的標頭檔“<iosfwd>”。他分外彰顯“本條款適用於templates也適用於non-templates”。許多建置環境中template定義式同常被置於標頭檔中,但也有某些建置環境允許tamplates定義式放在“非標頭檔中”,這樣就可以將“只含聲明式”的標頭檔提供給templates。

這種使用pimpl idiom的classes,往往被稱為Handle classes。

這種classes的辦法之一就是將他們的所有函數轉交給相應的實作類別(implementation classes)並由後者完成實際工作。

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

另一個製作Handle class的辦法是,令Person稱為一種特殊的abstract base class(抽象基類)稱為Interface classes。這種class的目的是詳細一一描述derived classes的介面,因此它通常不帶成員變數,也沒有建構函式,只有一個virtual解構函式以及一組pure virtual函數,又來敘述整個介面。

一個針對Person而寫的Interface class或許看起來像這樣:

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

這個class的客戶必須以Person的pointers和reference來撰寫應用程式,不能針對“內含pure virtual函數”的Person classes具現出實體。除非Interface class的介面被修改否則其客戶不需要重新編譯。

Interface class 的客戶必須有辦法為這種class建立新對象。它們通常調用一個特殊函數,此函數扮演一個“真正將被具現化”的那個derived class的建構函式角色。通常稱為工廠factory函數或virtual 建構函式。它們返回指標,指向動態分配所得對象,而該對象支援interface class的介面。這樣的函數又往往在interface class內被聲明為static:

class Person{
public:
    ...
    static std::tr1::shared_ptr<Person>
    create(const std::string& name, const Date& birthday, const Address& addr);
};

客戶可能會這樣使用它們:

std::string name;
Date dateBirth;
Address address;
std::tr1::shared_ptr<Person> pp(Person::create(name, dateBirth, address));
...
std::cout << pp->name()
            << "was born on "
            << PP->birthDate()
            << " and now lives at "
            << pp->address();
...

當然支援interface class介面的那個具象類(concrete classes)必須被定義出來,而真正的建構函式必須被調用。

假設有個derived class RealPerson,提供繼承而來的virtual函數的實現:

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;
    std::string birthDate() const;
    std::string address() const;
private:
    std::string theName;
    Date theBirthDate;
    Address theAddress;
};

有了RealPerson之後,寫出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));
}

一個更現實的Person::create實現代碼會建立不同類型的derived class對象,取決於諸如額外參數值、獨自檔案或資料庫的資料、環境變數等等。

RealPerson示範實現了Interface class的兩個最常見機制之一:從interface class繼承介面規格,然後實現出介面所覆蓋的函數。

handle classes 和 interface classes解除了介面和實現之間的耦合關係,從而降低檔案間的編譯依存性。

handle classed身上,成員函數必須通過implementation pointer取得對象資料。那會為每一次訪問增加一層間接性。每個對象消耗的記憶體必須增加一個implementation pointer的大小。implementation pointer必須初始化指向一個動態分配的implementation object,所以還得蒙受因動態記憶體分配兒帶來的額外開銷。

Interface classes,由於每個函數都是virtual,必須為每次函數調用付出一個間接跳躍。此外Interface class派生的對象必須內含一個vptr(virtual table pointer)。

在程式開發過程中使用handle class 和 interface class以求實現碼有所改變時對其客戶帶來最小衝擊。

而當他們導致速度和/或大小差異過於重大以至於class之間的耦合相形之下不成為關鍵時,就以具象類(concrete class)替換handle class 和 interface class。

支援“編譯依存最小化”的一般構想是:相依於聲明式,不要相依於定義式。

程式庫標頭檔應該以“安全且僅有聲明式”(full and declaration-only forms)的形式存在。這種做法不論是否涉及templates都適用。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

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.