《Effective C++ 中文版 第三版》讀書筆記
** 條款 31:將文件間的編譯依存關(guān)系降至最低 **
假設(shè)你對 C++ 程序的某個 class 實現(xiàn)文件做了些輕微改變,修改的不是接口,而是實現(xiàn),而且只改 private 成分。
然后重新建置這個程序,并預(yù)計只花數(shù)秒就好,當(dāng)按下 “Build” 或鍵入 make,會大吃一驚,因為你意識到整個世界都被重新編譯和鏈接了!
問題是在 C++ 并沒有把 “將接口從實現(xiàn)中分離” 做得很好。class 的定義式不只詳細敘述了 class 接口,還包括十足的實現(xiàn)細目:
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; //實現(xiàn)細目
Date theBirthDate; //實現(xiàn)細目
Address theAddress; //實現(xiàn)細目
};
這個 class Person 無法通過編譯,Person 定義文件的最上方可能存在這樣的東西:
#include <string>
#include "date.h"
#include "address.h"
這樣一來,便在 Person 定義文件和其含入文件之間形成了一種編譯依存關(guān)系(compilation dependency)。如果這些頭文件中有任何一個被改變,或這些文件所依賴的其他頭文件有任何改變,那么每個含入 Person class 的文件就得重新編譯,任何使用 Person class 的文件也必須重新編譯。這樣的的連串編譯依存關(guān)系(cascading compilation dependencies)會對許多項目造成難以形容的災(zāi)難。
為什么 C++ 堅持將 class 的實現(xiàn)細目置于 class 定義式中?為什么不這樣定義 Person,將實現(xiàn)細目分開敘述:
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 前置聲明并不正確,而且你本來就不應(yīng)該嘗試手工聲明一部分標(biāo)準(zhǔn)程序庫。你應(yīng)該僅僅使用適當(dāng)?shù)?#includes 完成目的。標(biāo)準(zhǔn)頭文件不太可能成為編譯瓶頸,
第二,編譯器必須在編譯期間知道對象的大小:
int main()
{
int x;
Person p(params);
}
編譯器知道必須分配足夠空間放置一個 Person,但是他必須知道一個 Person 對象多大,獲得這一信息的唯一辦法是詢問 class 定義式。然而,如果 class 定義式可以合法的不列出實現(xiàn)細目,編譯器如何知道該分配多少空間?
此問題在 smalltalk,java 等語言上并不存在,因為當(dāng)我們以那種語言定義對象時,編譯器只分配足夠空間給一個指針(用于指向該對象)使用。就是說它們將上述代碼視同這樣子:
int main()
{
int x;
Person* p;
}
這當(dāng)然也是合法的 C++ 代碼,所以你可以玩玩 “將對象實現(xiàn)細目隱藏在一個指針背后” 的游戲。可以把 Person 分割為兩個 classes,一個提供接口,另一個負責(zé)實現(xiàn)接口。負責(zé)實現(xiàn)的那個所謂的 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; // 指向?qū)崿F(xiàn)物的指針
};
這里,Person 只內(nèi)含一個指針成員,指向其實現(xiàn)類(PersonImpl)。這個設(shè)計常被稱為 pimpl idiom(pimpl 是 “pointer to implementation” 的縮寫)。
這樣,Person 的客戶就完全與 Date,Address 以及 Person 的實現(xiàn)細目分離了。那些 classes 的任何實現(xiàn)修改都不需要 Person 客戶端重新編譯。
這個分離的關(guān)鍵在于以 “聲明的依存性” 替換 “定義的依存性”,那正是編譯依存性最小化的本質(zhì):讓頭文件盡可能自我滿足,萬一做不到,則讓它與其他文件內(nèi)的聲明式(而非定義式)相依。其他每件事都源自于這個簡單的涉及策略。
如果用 object reference 或 object pointer 可以完成任務(wù),就不要用 objects。
可以只靠聲明式定義出指向該類型的 pointer 和 reference;但如果定義某類型的 objects,就需要用到該類型的定義式。
如果能夠,盡量以 class 聲明式替換 class 定義式。
當(dāng)你聲明一個函數(shù)而它用到某個 class 時,你并不需要該 class 的定義式,縱使函數(shù)以 by value 方式傳遞該類型的參數(shù)(或返回值)亦然:
class Date; // class 聲明式
Date today();
void clearAppiontments(Date d);
聲明 today 函數(shù)和 clearAppointments 函數(shù)無需定義 Date,但是一旦有任何人調(diào)用那些函數(shù),調(diào)用之前 Date 定義式一定得先曝光才行。如果能夠?qū)?“提供 class 定義式”(通過 #include 完成)的義務(wù)從 “函數(shù)聲明所在” 之頭文件移轉(zhuǎn)到 “內(nèi)含函數(shù)調(diào)用” 之客戶文件,便可將 “并非真正必要之類型定義” 與客戶端之間的編譯依存性去除掉。
為聲明式和定義式提供不同的頭文件。
因此程序庫客戶應(yīng)該總是 #inlcude 一個聲明文件而非前置聲明若干函數(shù),
#include "datefwd,h" // 這個頭文件內(nèi)聲明 class Date
Date today();
void clearAppointments(Date d);
只含聲明式的那個頭文件名為 “datefwd.h”,像標(biāo)準(zhǔn)程序庫的頭文件 “<iosfwd>”。他分外彰顯 “本條款適用于 templates 也適用于 non-templates”。許多建置環(huán)境中 template 定義式同常被置于頭文件中,但也有某些建置環(huán)境允許 tamplates 定義式放在 “非頭文件中”,這樣就可以將 “只含聲明式” 的頭文件提供給 templates。
這種使用 pimpl idiom 的 classes,往往被稱為 Handle classes。
這種 classes 的辦法之一就是將他們的所有函數(shù)轉(zhuǎn)交給相應(yīng)的實現(xiàn)類(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 的接口,因此它通常不帶成員變量,也沒有構(gòu)造函數(shù),只有一個 virtual 析構(gòu)函數(shù)以及一組 pure virtual 函數(shù),又來敘述整個接口。
一個針對 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 來撰寫應(yīng)用程序,不能針對 “內(nèi)含 pure virtual 函數(shù)” 的 Person classes 具現(xiàn)出實體。除非 Interface class 的接口被修改否則其客戶不需要重新編譯。
Interface class 的客戶必須有辦法為這種 class 創(chuàng)建新對象。它們通常調(diào)用一個特殊函數(shù),此函數(shù)扮演一個 “真正將被具現(xiàn)化” 的那個 derived class 的構(gòu)造函數(shù)角色。通常稱為工廠 factory 函數(shù)或 virtual 構(gòu)造函數(shù)。它們返回指針(或者更為可取的智能指針),指向動態(tài)分配所得對象,而該對象支持 interface class 的接口。這樣的函數(shù)又往往在 interface class 內(nèi)被聲明為 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();
...
當(dāng)然支持 interface class 接口的那個具象類(concrete classes)必須被定義出來,而真正的構(gòu)造函數(shù)必須被調(diào)用。
假設(shè)有個 derived class RealPerson,提供繼承而來的 virtual 函數(shù)的實現(xiàn):
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));
}
一個更現(xiàn)實的 Person::create 實現(xiàn)代碼會創(chuàng)建不同類型的 derived class 對象,取決于諸如額外參數(shù)值、獨自文件或數(shù)據(jù)庫的數(shù)據(jù)、環(huán)境變量等等。
RealPerson 示范實現(xiàn)了 Interface class 的兩個最常見機制之一:從 interface class 繼承接口規(guī)格,然后實現(xiàn)出接口所覆蓋的函數(shù)。
handle classes 和 interface classes 解除了接口和實現(xiàn)之間的耦合關(guān)系,從而降低文件間的編譯依存性。
handle classed 身上,成員函數(shù)必須通過 implementation pointer 取得對象數(shù)據(jù)。那會為每一次訪問增加一層間接性。每個對象消耗的內(nèi)存必須增加一個 implementation pointer 的大小。 implementation pointer 必須初始化指向一個動態(tài)分配的 implementation object,所以還得蒙受因動態(tài)內(nèi)存分配兒帶來的額外開銷。
Interface classes,由于每個函數(shù)都是 virtual,必須為每次函數(shù)調(diào)用付出一個間接跳躍。此外 Interface class 派生的對象必須內(nèi)含一個 vptr(virtual table pointer)。
在程序開發(fā)過程中使用 handle class 和 interface class 以求實現(xiàn)碼有所改變時對其客戶帶來最小沖擊。
而當(dāng)他們導(dǎo)致速度和/或大小差異過于重大以至于 class 之間的耦合相形之下不成為關(guān)鍵時,就以具象類(concrete class)替換 handle class 和 interface class。
** 請記住: **
- 支持 “編譯依存最小化” 的一般構(gòu)想是:相依于聲明式,不要相依于定義式。基于此構(gòu)想的兩個手段是 Handle class 和 Interface class。
- 程序庫頭文件應(yīng)該以 “完全且僅有聲明式”(full and declaration-only forms)的形式存在。這種做法不論是否涉及 templates 都適用。