2008-09-13 46 views
104

背景:爲什麼要使用「PIMPL」習語?

PIMPL Idiom(指針實現)是用於執行隱藏在其中一個公共類包裝的結構或類,可以不在庫的公共類是的一部分外部看到的技術。

這隱藏了庫的用戶的內部實現細節和數據。

實現此習語時,爲什麼要將公共方法放在pimpl類而不是公共類上,因爲公共類方法實現將編譯到庫中,而用戶只有頭文件?

爲了說明這段代碼,我們將Purr()實現放在impl類上,並將其包裝。

爲什麼不直接在公共課上實施Purr?

// header file: 
class Cat { 
    private: 
     class CatImpl; // Not defined here 
     CatImpl *cat_; // Handle 

    public: 
     Cat();   // Constructor 
     ~Cat();   // Destructor 
     // Other operations... 
     Purr(); 
}; 


// CPP file: 
#include "cat.h" 

class Cat::CatImpl { 
    Purr(); 
...  // The actual implementation can be anything 
}; 

Cat::Cat() { 
    cat_ = new CatImpl; 
} 

Cat::~Cat() { 
    delete cat_; 
} 

Cat::Purr(){ cat_->Purr(); } 
CatImpl::Purr(){ 
    printf("purrrrrr"); 
} 
+26

因爲PIMP習語應該避免嗎?.. – mlvljr 2010-10-07 21:31:23

+1

優秀的答案,我發現這個鏈接也包含全面的信息以及:http://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl/ – zhanxw 2013-02-18 14:24:46

+11

如果你想要做維護編碼器來幫助你,請記住這是一個**接口**模式。不要將它用於每個內部課堂。引用刀鋒亞軍,我見過你們不會相信的事情。 – DevSolar 2013-03-11 14:24:39

回答

34
  • 因爲您希望Purr()能夠使用CatImpl的私人成員。沒有friend申明,Cat::Purr()將不允許這樣的訪問。
  • 因爲你不混合職責:一個班級實施,一個班級轉發。
2

配售調用impl-> PURR內cpp文件意味着,在未來,你可以做一些事情,而不必更改頭文件完全不同。也許明年他們會發現他們可能調用的幫助器方法,以便他們可以更改代碼來直接調用該方法,而不是使用impl-> Purr。 (是的,他們可以通過更新實際的impl :: Purr方法來達到同樣的效果,但是在這種情況下,你會遇到一個額外的函數調用,只能依次調用下一個函數)

它也意味着標題只有定義,並沒有任何實現,這使得更清晰的分離,這是成語的整個點。

50

我想大多數人都把這稱爲「句柄正文」。請參閱James Coplien的高級C++編程風格和習語(Amazon link)。它也被稱爲Cheshire Cat,因爲劉易斯卡羅爾的角色會消失,直到只有咧嘴一笑。

示例代碼應分佈在兩組源文件中。那麼只有Cat.h是產品附帶的文件。

CatImpl.h包含在Cat.cpp中,CatImpl.cpp包含CatImpl :: Purr()的實現。這對於使用您的產品的公衆是不可見的。

基本上這個想法是儘可能地隱藏實施過程中窺探的目光。 這是最有用的地方,你有一個商業產品作爲一系列的庫,通過客戶的代碼編譯和鏈接到一個API訪問的庫。

我們與IONAs Orbix的3.3產品在2000年

重寫這樣做正如其他人所提到的,用他的技術完全解耦對象的接口的實現。那麼如果你只是想改變Purr()的實現,你將不必重新編譯使用Cat的所有東西。

該技術用於名爲design by contract的方法。

6

通常,對於Owner類(本例中爲Cat)的頭部中唯一對Pimpl類的引用將是前向聲明,因爲這可以大大減少依賴關係。例如,如果您的Pimpl類具有ComplicatedClass作爲成員(而不僅僅是指向它的指針或引用),那麼您需要在使用ComplicatedClass之前完全定義它。實際上,這意味着包含「ComplicatedClass.h」(它也間接包含任何ComplicatedClass依賴的)。這可能導致單個頭部填充拉動很多很多東西,這對管理你的依賴關係(和你的編譯時間)是不利的。

當您使用pimpl idion時,您只需#include所有者類型的公共接口中使用的東西(這裏將是Cat)。這使得使用你的圖書館的人變得更好,並且意味着你不需要擔心取決於你圖書館某些內部部分的人 - 無論是錯誤的,還是因爲他們想做一些你不允許的事情,所以他們#define包括你的文件之前的私人公衆。

如果它是一個簡單的類,通常有沒有理由使用一個平普爾,但時候的類型是相當大的,它可以是一個很大的幫助(尤其是在避免長建倍)

14

如果您的課程使用pimpl習語,您可以避免更改公共課程的頭文件。

這允許您在不修改外部類的頭文件的情況下向pimpl類添加/刪除方法。你也可以添加/刪除#includes到pimpl。

當您更改外部類的頭文件,你必須重新編譯來#include一切努力(而如果這些都頭文件,你必須重新編譯的一切,來#include他們,等等)

0

我不知道這是一個差異值得一提的,但...

纔有可能有其自己的命名空間中的執行和對代碼的公開包裝/庫命名空間的用戶看到:

catlib::Cat::Purr(){ cat_->Purr(); } 
cat::Cat::Purr(){ 
    printf("purrrrrr"); 
} 

這樣所有的lib rary代碼可以使用cat命名空間,並且隨着向用戶公開類的需要出現,可以在catlib命名空間中創建一個包裝器。

0

我發現它告訴我們,儘管pimpl習語衆所周知,但我並不認爲它在現實生活中經常出現(例如在開源項目中)。

我經常想知道「好處」是否被誇大了;是的,你可以使你的一些實現細節更加隱藏,是的,你可以在不改變標題的情況下改變你的實現,但這並不明顯,這些都是現實中的巨大優勢。

也就是說,目前尚不清楚您的實施是否需要以及隱藏得很好,也許很少有人真的只改變實現;只要你需要添加新的方法,比如說,你需要改變標題。

1

我剛剛在過去的幾天裏實施了我的第一個pimpl課程。我用它來消除我在Borland Builder中遇到的問題,包括winsock2.h。這似乎是搞砸了結構對齊,因爲我在類私有數據中有套接字的東西,這些問題傳播到包含頭的任何cpp文件。

通過使用pimpl,winsock2.h只包含在一個cpp文件中,我可以對此問題進行解決,而不用擔心它會回來咬我。

爲了回答最初的問題,我發現在將調用轉發給pimpl類時發現的優點是,pimpl類與您在初次使用它之前的原始類相同,再加上您的實現不符合「以一些奇怪的方式分佈在兩個班上。實施公衆簡單地向前進入扁桃體班更加清楚。

像納特先生說的,一個班級,一個責任。

3

那麼,我不會使用它。我有一個更好的選擇:

了foo.h:

class Foo { 
public: 
    virtual ~Foo() { } 
    virtual void someMethod() = 0; 

    // This "replaces" the constructor 
    static Foo *create(); 
} 

Foo.cpp中:

namespace { 
    class FooImpl: virtual public Foo { 

    public: 
     void someMethod() { 
      //.... 
     }  
    }; 
} 

Foo *Foo::create() { 
    return new FooImpl; 
} 

請問這種模式有名字嗎?

作爲Python和Java程序員,我比pImpl習慣用法更喜歡它。

17

值得一提的是,它將實現從接口中分離出來。這在小型項目中通常不是很重要。但是,在大型項目和圖書館中,它可以用來顯着縮短構建時間。

考慮到Cat的實現可能包含很多頭文件,可能會涉及模板元編程,這需要花時間自行編譯。爲什麼只是想使用Cat的用戶必須包含所有這些內容?因此,使用pimpl習語(因此前向聲明CatImpl)隱藏了所有必需的文件,並且使用該界面不會強制用戶包含它們。

我正在開發一個用於非線性優化的庫(閱讀「很多討厭的數學」),它是在模板中實現的,所以大部分代碼都在頭文件中。大約需要五分鐘的時間才能編譯(在一個體面的多核CPU上),並且只需解析頭文件就可以花費大約一分鐘的時間。所以使用這個庫的人每次編譯他們的代碼時都要等幾分鐘,這使得開發相當於tedious。但是,通過隱藏實現和頭文件,只需包含一個簡單的接口文件即可立即編譯。

它不一定與保護實現免受其他公司拷貝有關 - 這些不會發生,除非您的算法的內部運作可以從成員變量的定義中猜到(如果所以,它可能不是很複雜,首先不值得保護)。

1

我們使用PIMPL習語來模擬面向方面的編程,其中在執行成員函數之前和之後調用pre,post和error方面。

struct Omg{ 
    void purr(){ cout<< "purr\n"; } 
}; 

struct Lol{ 
    Omg* omg; 
    /*...*/ 
    void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } } 
}; 

我們還使用指向基類的指針來分享許多類之間的不同方面。

這種方法的缺點是庫用戶必須考慮將要執行的所有方面,但只能看到他的類。它需要瀏覽文檔中的任何副作用。