舊話重提:pimpl慣用手法的背後
劉未鵬
pimpl
慣用手法已經太老了,老得人們已經記不得它是什麼時候被提出的了。像這麼乙個老得牙都掉了的東東幾乎是肯定講不出什麼新意出來的。
本文也不例外,只不過,這裡我們並不想提出什麼新的創意,而是對
pimpl
背後的機制作乙個**和總結。
城門失火殃及池魚
pimpl
慣用手法的運用方式大家都很清楚,其主要作用是解開類的使用介面和實現的耦合。如果不使用
pimpl
慣用手法,**會像這樣:
//c.hpp
#include
class c
;像上面這樣的**,
c與它的實現就是強耦合的,從語義上說,
x成員資料是屬於
c的實現部分,不應該暴露給使用者。從語言的本質上來說,在使用者的**中,每一次使用
」new c」
和」c c
1」這樣的語句,都會將
x的大小硬編碼到編譯後的二進位制**段中(如果
x有虛函式,則還不止這些)——這是因為,對於
」new c」
這樣的語句,其實相當於
operator new(sizeof(c) )
後面再跟上
c的建構函式,而
」c c
1」則是在當前棧上騰出
sizeof(c)
大小的空間,然後呼叫
c的建構函式。因此,每次
x類作了改動,使用
c.hpp
的原始檔都必須重新編譯一次,因為
x的大小可能改變了。
在乙個大型的專案中,這種耦合可能會對
build
時間產生相當大的影響。
pimpl
慣用手法可以將這種耦合消除,使用
pimpl
慣用手法的**像這樣:
//c.hpp
class x;//
用前導宣告取代
include
class c
;在乙個既定平台上,任何指標的大小都是相同的。之所以分為x*,
y*這些各種各樣的指標,主要是提供乙個高層的抽象語義,即該指標到底指向的是那個類的物件,並且,也給編譯器乙個指示,從而能夠正確的對使用者進行的操作(如呼叫
x的成員函式)決議並檢查。但是,如果從執行期的角度來說,每種指標都只不過是個
32位的長整型(如果在
64位機器上則是
64位,根據當前硬體而定)。
正由於pimpl
是個指標,所以這裡
x的二進位制資訊(
sizeof(c)
等)不會被耦合到
c的使用介面上去,也就是說,當使用者
」new c」
或」c c
1」的時候,編譯器生成的**中不會摻雜
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背後的機制作乙個 和總結。城門失火殃及池魚 pimpl...