イディオムPimplおよびFast Pimpl-実装へのポインター

他の名前: Bridge、Compilation Firewall、Handle / Body

ソケットを使用してクロスプラットフォームネットワークアプリケーションを作成する必要があるとしましょう。 これを行うには、特定のプラットフォーム(「非表示クラス」)の実装の詳細をカプセル化するGeneralSocketクラス(「Visibleクラス」)が必要です。 多くの場合、実装の詳細をユーザーまたは他の開発者から隠す必要があります。





例を考えてみましょう:

//GeneralSocket.h

#include “UnixSocketImpl.h”



Class GeneralSocket{

public :

connect();

private :

UnixSocketImpl socket;

}



//GeneralSocket.cxx

GeneralSocket::connect(){

socket.connectImpl();

}




* This source code was highlighted with Source Code Highlighter .








この例では、コンパイル段階で非表示のUnixSocketImplクラスの記述を知っておく必要があります。 さらに、ユーザーがUnixSocketImplクラスの関数を使用して、表示されているGeneralSocketクラスをバイパスすることを妨げるものはありません。 次に、可視クラスのプライベートメンバーをポインターに置き換え、ヘッダーファイルから非表示クラスUnixSocketImplの説明を削除してみましょう。



//GeneralSocket.h

Class UnixSocketImpl;



Class GeneralSocket

{

public :

GeneralSocket();

void connect();

private :

UnixSocketImpl * socket;

}



//GeneralSocket.cxx

#include “UnixSocketImpl.h”



GeneralSocket::GeneralSocket() : socket ( new UnixSocketImpl){}



GeneralSocket::~GeneralSocket() {

delete socket;

socket = 0;

}



void GeneralSocket::connect() {

socket->connectImpl();

}




* This source code was highlighted with Source Code Highlighter .








ヘッダーファイルのUnixSocketImpl.hを取り除き、GeneralSocketクラスの実装ファイルに転送しました。 これで、ユーザーは特定の実装に到達できなくなり、GeneralSocketクラスのインターフェイスを介してのみ機能を使用できるようになります。



C ++では、クラスが変更された場合(プライベートメンバー関数であっても)、このクラスのすべてのユーザーを再コンパイルする必要があります。 このような依存関係を回避するために、メンバー関数へのポインターが使用され、その実装は非表示にする必要があります。 この手法はPimpl(実装へのポインター-実装へのポインター)と呼ばれます。 2つの主な欠点は次のとおりです。

  1. オブジェクトを作成するたびに、参照するオブジェクトの動的メモリ割り当てが必要になります
  2. 隠しオブジェクトのメンバーにアクセスするために、複数レベル(少なくとも1つ)の間接参照を使用する


何を隠そうとしますか?

  1. 非表示のメンバーデータのみ
  2. すべての非表示のメンバーデータとメンバー関数。 残念ながら、派生クラスから見える必要があるため、仮想関数を非表示にすることはできません。 また、閉じたクラスでは、その関数を使用するために開いているクラスへの参照が必要になる場合があります
  3. プライベートで安全なメンバー。 残念ながら、保護されたメンバーは派生クラスからアクセスできる必要があるため、非表示にすることはできません
  4. クラス全体。 利点は、プライベートクラスがパブリッククラスへのポインタを必要としないことです。 一方、遺産を失う


次に、タスクを少し複雑にしましょう。 私たちのクラスが非常に頻繁に使用されていることを想像してください。ご存知のように、ヒープ上の動的メモリ割り当ては非常に高価な操作です。 事前に隠しオブジェクトにメモリを割り当ててみましょう:

//GeneralSocket.h

Class GeneralSocket

{

public :

GeneralSocket();

void connect();

private :

static const size_t sizeOfImpl = 42; /* or whatever space needed*/

char socket [sizeOfImpl];

}



//GeneralSocket.cxx

#include “UnixSocketImpl.h”



GeneralSocket::GeneralSocket() : {

assert(sizeOfImpl >= sizeof (UnixSocketImpl));

new (&socket[0]) UnixSocketImpl;

}



GeneralSocket::~GeneralSocket() {

(reinterpret_cast<UnixSocketImpl *> (&socket[0]))->~UnixSocketImpl();

}



GeneralSocket::connect() {

socket->connectImpl();

}




* This source code was highlighted with Source Code Highlighter .








GeneralSocketヘッダーファイルのUnixSocketImplクラス宣言を取り除き、動的メモリ割り当てを取り除きました。 その見返りとして、次のような重大な欠点がいくつかありました。

  1. C ++は強く型付けされた言語であり、このトリックは言語の制限を回避する試みです
  2. メモリアライメントの問題。 このメソッドは、UnixSocketImplのすべてのメンバーに対してメモリが適切に配置されることを保証しません。 完全な移植性を保証するわけではないが、ほとんどの場合でも機能するソリューションは、ユニオンを使用することです。

    union max_align {

    void * dummy1;

    int dymmy2;

    }



    union {

    max_align m;

    char socket [sizeOfImpl];

    }




    * This source code was highlighted with Source Code Highlighter .






  3. UnixSocketImpl関数の慎重な処理。 たとえば、コピーオペレータの割り当てを使用するか、デフォルトのオペレータを無効にする必要があります。
  4. GeneralSocketクラスのサポートには時間がかかります。 ここで、sizeOfImplサイズを最新の状態に保つ必要があります。
  5. 条件sizeOfImpl> = sizeof(UnixSocketImpl)が満たされない場合の非効率的なメモリ使用量


ご存じのように、固定サイズのメモリの割り当ては、任意の量のメモリの割り当てよりもはるかに高速です。 メモリアロケータを使用してみましょう。

//GeneralSocket.h

class GSimpl;



class GeneralSocket {

private :

GSimpl * pimpl;

}



//GeneralSocket.cxx

#include “UnixSocketImpl.h”



class FixedAllocator {

public :

static FixedAllocator* Instance();

void * Allocate(size_t);

void Deallocate( void *);

private :

/*Singleton implementation that allocates memory of fixed size*/

};



struct FastPimpl {



void * operator new ( size_t s) {

return FixedAllocator::Instance()->Allocate(s);

}



void operator delete( void * p) {

FixedAllocator::Instance()->Deallocate(p);

}

};



struct GSimpl : FastPimpl {

/*use UnixSocketImpl here*/

};



GeneralSocket::GeneralSocket() : pimpl ( new GSimpl){}



GeneralSocket::~GeneralSocket() {

delete pimpl;

pimpl = 0;

}




* This source code was highlighted with Source Code Highlighter .








FastPimplを継承すると、固定サイズのメモリアロケーター(FixedAllocator)で必要な閉じたクラスを取得できます。

まとめると。 Pimplを使用できる場合:

  1. 実装から抽象化を分離する必要がある場合。 この場合、必要な実装は、たとえばプログラムの開始時に選択できます。
  2. 抽象化と実装の両方を新しいクラスで拡張し、それらを任意に組み合わせる必要がある場合。 したがって、抽象化と実装のクラスの2つの独立した階層を取得します。
  3. 実装を変更するときにクライアントコードを再コンパイルしない場合
  4. 抽象化の実装をクライアントから隠す必要がある場合




関連リンク

  1. スティーブンC.デューハースト。 「滑りやすいC ++を配置します。 プログラムの設計およびコンパイル時の問題の回避。」(C ++落とし穴。コーディングおよび設計の一般的な問題の回避)。 「ヒント8」
  2. サッターの紋章、アンドレイ・アレクサンドレスク。 「C ++プログラミング標準。」 「第43章」
  3. オブジェクト指向設計のレセプション。 デザインパターン、E。ガンマ、R。ヘルム、R。ジョンソン、J。ヴリスサイド。 ブリッジパターン
  4. コンパイルファイアウォール(http://gotw.ca/gotw/024.htm)
  5. Fast Pimpl Idiom(http://gotw.ca/gotw/028.htm)



All Articles