舊話重提 pImpl慣用手法的背後

2021-04-16 05:37:36 字數 3687 閱讀 3980

舊話重提: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...