C++ pimpl
---------摘自《超越c++標(biāo)準(zhǔn)庫——boost程序庫導(dǎo)論》
- 舊話重提:pImpl慣用手法的背后???? 摘自?pongba?的 Blog
pImpl慣用手法已經(jīng)太老了,老得人們已經(jīng)記不得它是什么時候被提出的了。像這么一個老得牙都掉了的東東幾乎是肯定講不出什么新意出來的。
本文也不例外,只不過,這里我們并不想提出什么新的創(chuàng)意,而是對pImpl背后的機(jī)制作一個探究和總結(jié)。
城門失火 殃及池魚
pImpl慣用手法的運(yùn)用方式大家都很清楚,其主要作用是解開類的使用接口和實(shí)現(xiàn)的耦合。如果不使用pImpl慣用手法,代碼會像這樣:
?????? //c.hpp
??????? #include<x.hpp>
class C
??????? {
??????? public:
??????????? void f1();
??????? private:
??????????? X x; //與X的強(qiáng)耦合
??????? };
像上面這樣的代碼,C與它的實(shí)現(xiàn)就是強(qiáng)耦合的,從語義上說,x成員數(shù)據(jù)是屬于C的實(shí)現(xiàn)部分,不應(yīng)該暴露給用戶。從語言的本質(zhì)上來說,在用戶的代碼中,每一次使用”new C”和”C c1”這樣的語句,都會將X的大小硬編碼到編譯后的二進(jìn)制代碼段中(如果X有虛函數(shù),則還不止這些)——這是因為,對于”new C”這樣的語句,其實(shí)相當(dāng)于operator new(sizeof(C) )后面再跟上C的構(gòu)造函數(shù),而”C c1”則是在當(dāng)前棧上騰出sizeof(C)大小的空間,然后調(diào)用C的構(gòu)造函數(shù)。因此,每次X類作了改動,使用c.hpp的源文件都必須重新編譯一次,因為X的大小可能改變了。
在一個大型的項目中,這種耦合可能會對build時間產(chǎn)生相當(dāng)大的影響。
pImpl慣用手法可以將這種耦合消除,使用pImpl慣用手法的代碼像這樣:
??????? //c.hpp
??????? class X;? //用前導(dǎo)聲明取代include
??????? class C
??????? {
???????? ...
???????? private:
??????????? X* pImpl; //聲明一個X*的時候,class X不用完全定義
??????? };
在一個既定平臺上,任何指針的大小都是相同的。之所以分為X*,Y*這些各種各樣的指針,主要是提供一個高層的抽象語義,即該指針到底指向的是那個類的對象,并且,也給編譯器一個指示,從而能夠正確的對用戶進(jìn)行的操作(如調(diào)用X的成員函數(shù))決議并檢查。但是,如果從運(yùn)行期的角度來說,每種指針都只不過是個32位的長整型(如果在64位機(jī)器上則是64位,根據(jù)當(dāng)前硬件而定)。
正由于pImpl是個指針,所以這里X的二進(jìn)制信息(sizeof(C)等)不會被耦合到C的使用接口上去,也就是說,當(dāng)用戶”new C”或”C c1”的時候,編譯器生成的代碼中不會摻雜X的任何信息,并且當(dāng)用戶使用C的時候,使用的是C的接口,也與X無關(guān),從而X被這個指針徹底的與用戶隔絕開來。只有C知道并能夠操作pImpl成員指向的X對象。
防火墻
“修改X的定義會導(dǎo)致所有使用C的源文件重新編譯”這種事就好比“城門失火,殃及池魚”,其原因是“護(hù)城河”離“城門”太近了(耦合)。
pImpl慣用手法又被成為“編譯期防火墻”,什么是“防火墻”,指針?不是。C++的編譯模式為“分離式編譯”,即不同的源文件是分開編譯的。也就是說,不同的源文件之間有一道天然的防火墻,一個源文件“失火”并不會影響到另一個源文件。
但是,這里我們考慮的是頭文件,如果頭文件“失火”又當(dāng)如何呢?頭文件是不能直接編譯的,它包含于源文件中,并作為源文件的一部分被一起編譯。
這也就是說,如果源文件S.cpp使用了C.hpp,那么class C的(接口部分的)變動將無可避免的導(dǎo)致S.CPP的重新編譯。但是作為class C的實(shí)現(xiàn)部分的class X卻完全不應(yīng)該導(dǎo)致S.cpp的重新編譯。
因此,我們需要把class X隔絕在C.hpp之外。這樣,每個使用class C的源文件都與class X隔離開來(與class X不在同一個編譯單元)。但是,既然class C使用了class X的對象來作為它的實(shí)現(xiàn)部分,就無可避免的要“依賴”于class X。只不過,這個“依賴”應(yīng)該被描述為:“class C的實(shí)現(xiàn)部分依賴于class X”,而不應(yīng)該是“class C的用戶使用接口部分依賴于class X”。
如果我們直接將X的對象寫在class C的數(shù)據(jù)成員里面,則顯而易見,使用class C的用戶“看到”了不該“看到”的東西——class X——它們之間產(chǎn)生了耦合。然而,如果使用一個指向class X的指針,就可以將X的二進(jìn)制信息“推”到class C的實(shí)現(xiàn)文件中去,在那里,我們#include”x.hpp”,定義所有的成員函數(shù),并依賴于X的實(shí)現(xiàn),這都無所謂,因為C的實(shí)現(xiàn)本來就依賴于X,重要的是:此時class X的改動只會導(dǎo)致class C的實(shí)現(xiàn)文件重新編譯,而用戶使用class C的源文件則安然無恙!
??? 指針在這里充當(dāng)了一座橋。將依賴信息“推”到了另一個編譯單元,與用戶隔絕開來。而防火墻是C++編譯器的固有屬性。
穿越C++編譯期防火墻
是什么穿越了C++編譯期防火墻?是指針!使用指針的源文件“知道”指針?biāo)傅氖鞘裁磳ο?#xff0c;但是不必直接“看到”那個對象——它可能在另一個編譯單元,是指針穿越了編譯期防火墻,連接到了那個對象。
從某種意義上說,只要是代表地址的符號都能夠穿越C++編譯期防火墻,而代表結(jié)構(gòu)(constructs)的符號則不能。
??? 例如函數(shù)名,它指的是函數(shù)代碼的始地址,所以,函數(shù)能夠聲明在一個編譯單元,但定義在另一個編譯單元,編譯器會負(fù)責(zé)將它們連接起來。用戶只要得到函數(shù)的聲明就可以使用它。而類則不同,類名代表的是一個語言結(jié)構(gòu),使用類,必須知道類的定義,否則無法生成二進(jìn)制代碼。變量的符號實(shí)質(zhì)上也是地址,但是使用變量一般需要變量的定義,而使用extern修飾符則可以將變量的定義置于另一個編譯單元中。
總結(jié)
- 上一篇: c++以代理的方式来实现接口化编程
- 下一篇: c++中的pod类型