明智地使用Pimpl

2021-06-09 03:00:13 字數 3021 閱讀 4127

明智地使用pimpl

首先引用一下別人的內容

pimpl 用法背後的思想是把客戶與所有關於類的私有部分的知識隔離開。由於客戶是依賴於類的標頭檔案的,標頭檔案中的任何變化都會影響客戶,即使僅是對私有節或保護節的修改。pimpl用法隱藏了這些細節,方法是將私有資料和函式放入乙個單獨的類中,並儲存在乙個實現檔案中,然後在標頭檔案中對這個類進行前向宣告並儲存乙個指向該實現類的指標。類的建構函式分配這個pimpl類,而析構函式則釋放它。這樣可以消除標頭檔案與實現細節的相關性。

---------摘自《超越c++標準庫——boost程式庫導論》

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修飾符則可以將變數的定義置於另乙個編譯單元中。

以下是引自c++程式設計規範的內容來作一些補充

1、物件的指標最好是選擇合適的智慧型指標

2、即使私有成員函式不能從類外及其他友元呼叫,它們也會參加名字查詢 過載解析,因此會使呼叫無效或者存在二義性

如下面的例子:

int  fun(int);  //1

class calc

};calc c;

c.fun("hello"); //錯誤,3不可訪問(2沒有問題,但是不能考慮,因為3是更好的匹配),解決:c.fun(string("hello"))

當然這些問題也可以用pimpl來解決;但也要永遠記住不寫成員函式的私有過載。

條款42 明智地使用私有繼承

條款35說明,c 將公有繼承視為 是乙個 的關係。它是通過這個例子來證實的 假如某個類層次結構中,student類從person類公有繼承,為了使某個函式成功呼叫,編譯器可以在必要時隱式地將student轉換為person。這個例子很值得再看一遍,只是現在,公有繼承換成了私有繼承 class per...

條款42 明智地使用私有繼承

第乙個規則是,和公有繼承相反,如果兩個類之間的繼承關係為私有,編譯器一般不會將派生類物件 如student 轉換成基類物件 如person 第二個規則是,從私有基類繼承而來的成員都成為了派生類的私有成員,即使它們在基類中是保護或公有成員,即派生類物件不能訪問基類的所有成員 class person ...

Item 39 明智地使用 private 繼承

public 繼承表示 is a 的關係,這是因為編譯器會在需要的時候將子類物件隱式轉換為父類物件。然而 private 繼承則不然 class person class student private person void eat const person p person p student ...