舊話重提:pimpl慣用手法的背後
劉未鵬
pimpl慣用手法已經太老了,老得人們已經記不得它是什麼時候被提出的了。像這麼乙個老得牙都掉了的東東幾乎是肯定講不出什麼新意出來的。
本文也不例外,只不過,這裡我們並不想提出什麼新的創意,而是對pimpl背後的機制作乙個**和總結。
城門失火殃及池魚
pimpl慣用手法的運用方式大家都很清楚,其主要作用是解開類的使用介面和實現的耦合。如果不使用pimpl慣用手法,**會像這樣:
#include
class c
;像上面這樣的**,c與它的實現就是強耦合的,從語義上說,x成員資料是屬於c的實現部分,不應該暴露給使用者。從語言的本質上來說,在使用者的**中,每一次使用」new c」和」c c1」
這樣的語句,都會將x的大小硬編碼到編譯後的二進位制**段中(如果x有虛函式,則還不止這些)——這是因為,對於」new c」這樣的語句,其實相當於operator new(sizeof(c) )後面再跟上c的建構函式,而」c c1」
則是在當前棧上騰出sizeof(c)大小的空間,然後呼叫c的建構函式。因此,每次x類作了改動,使用c.hpp的原始檔都必須重新編譯一次,因為x的大小可能改變了。
在乙個大型的專案中,這種耦合可能會對build時間產生相當大的影響。
pimpl慣用手法可以將這種耦合消除,使用pimpl慣用手法的**像這樣:
class x; //用前導宣告取代include
class c
;在乙個既定平台上,任何指標的大小都是相同的。之所以分為x*,y*這些各種各樣的指標,主要是提供乙個高層的抽象語義,即該指標到底指向的是那個類的物件,並且,也給編譯器乙個指示,從而能夠正確的對使用者進行的操作(如呼叫x的成員函式)決議並檢查。但是,如果從執行期的角度來說,每種指標都只不過是個32位的長整型(如果在64位機器上則是64位,根據當前硬體而定)。
正由於pimpl是個指標,所以這裡x的二進位制資訊(sizeof(c)等)不會被耦合到c的使用介面上去,也就是說,當使用者」new c」或」c c1」
的時候,編譯器生成的**中不會摻雜x的任何資訊,並且當使用者使用c的時候,使用的是c的介面,也與x無關,從而x被這個指標徹底的與使用者隔絕開來。只有c知道並能夠操作pimpl成員指向的x物件。
防火牆
「修改x的定義會導致所有使用c的原始檔重新編譯」這種事就好比「城門失火,殃及池魚」,其原因是「護城河」離「城門」太近了(耦合)。
pimpl慣用手法又被成為「編譯期防火牆」,什麼是「防火牆」,指標?不是。c++的編譯模式為「分離式編譯」,即不同的原始檔是分開編譯的。也就是說,不同的原始檔之間有一道天然的防火牆,乙個原始檔「失火」並不會影響到另乙個原始檔。
但是,這裡我們考慮的是標頭檔案,如果標頭檔案「失火」又當如何呢?標頭檔案是不能直接編譯的,它包含於原始檔中,並作為原始檔的一部分被一起編譯。
這也就是說,如果原始檔s.cpp使用了c.hpp,那麼class c的(介面部分的)變動將無可避免的導致s.cpp的重新編譯。但是作為class c的實現部分的class x卻完全不應該導致s.cpp的重新編譯。
因此,我們需要把class x隔絕在c.hpp之外。這樣,每個使用class c的原始檔都與class x隔離開來(與class x不在同乙個編譯單元)。但是,既然class c使用了class x的物件來作為它的實現部分,就無可避免的要「依賴」於class x。只不過,這個「依賴」應該被描述為:「class c的實現部分依賴於class x」,而不應該是「class c的使用者使用介面部分依賴於class x」。
如果我們直接將x的物件寫在class c的資料成員裡面,則顯而易見,使用class c的使用者「看到」了不該「看到」的東西——class x——它們之間產生了耦合。然而,如果使用乙個指向class x的指標,就可以將x的二進位制資訊「推」到class c的實現檔案中去,在那裡,我們#include」x.hpp」,定義所有的成員函式,並依賴於x的實現,這都無所謂,因為c的實現本來就依賴於x,重要的是:此時class x的改動只會導致class c的實現檔案重新編譯,而使用者使用class c的原始檔則安然無恙!
指標在這裡充當了一座橋。將依賴資訊「推」到了另乙個編譯單元,與使用者隔絕開來。而防火牆是c++編譯器的固有屬性。
穿越c++編譯期防火牆
是什麼穿越了c++編譯期防火牆?是指標!使用指標的原始檔「知道」指標所指的是什麼物件,但是不必直接「看到」那個物件——它可能在另乙個編譯單元,是指標穿越了編譯期防火牆,連線到了那個物件。
從某種意義上說,只要是代表位址的符號都能夠穿越c++編譯期防火牆,而代表結構(constructs)的符號則不能。
例如函式名,它指的是函式**的始位址,所以,函式能夠宣告在乙個編譯單元,但定義在另乙個編譯單元,編譯器會負責將它們連線起來。使用者只要得到函式的宣告就可以使用它。而類則不同,類名代表的是乙個語言結構,使用類,必須知道類的定義,否則無法生成二進位制**。變數的符號實質上也是位址,但是使用變數一般需要變數的定義,而使用extern修飾符則可以將變數的定義置於另乙個編譯單元中。
舊話重提 pImpl慣用手法的背後
舊話重提 pimpl慣用手法的背後 劉未鵬 pimpl 慣用手法已經太老了,老得人們已經記不得它是什麼時候被提出的了。像這麼乙個老得牙都掉了的東東幾乎是肯定講不出什麼新意出來的。本文也不例外,只不過,這裡我們並不想提出什麼新的創意,而是對 pimpl 背後的機制作乙個 和總結。城門失火殃及池魚 pi...