pimpl 用法

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

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

 

 

 

  • 舊話重提:pImpl慣用手法的背後     摘自 pongba 的 Blog

 

pImpl慣用手法已經太老了,老得人們已經記不得它是什麼時候被提出的了。像這麼一個老得牙都掉了的東東幾乎是肯定講不出什麼新意出來的。

本文也不例外,只不過,這裏我們並不想提出什麼新的創意,而是對pImpl背後的機制作一個探究和總結。

 

城門失火 殃及池魚

pImpl慣用手法的運用方式大家都很清楚,其主要作用是解開類的使用接口和實現的耦合。如果不使用pImpl慣用手法,代碼會像這樣:

       //c.hpp

        #include<x.hpp>

class C

        {

        public:

            void f1();

        private:

            X x; //X的強耦合

        };

像上面這樣的代碼,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慣用手法的代碼像這樣:

        //c.hpp

        class X;  //用前導聲明取代include

        class C

        {

         ...

         private:

            X* pImpl; //聲明一個X*的時候,class X不用完全定義

        };

在一個既定平臺上,任何指針的大小都是相同的。之所以分爲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修飾符則可以將變量的定義置於另一個編譯單元中。

 

發佈了25 篇原創文章 · 獲贊 9 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章