pImplイディオムは実際に実際に使用されていますか?


165

私はHerb Sutterの「Exceptional C ++」という本を読んでいます。その本で、pImplイディオムについて学びました。基本的には、のprivateオブジェクトの構造を作成し、classそれらを動的に割り当てて、コンパイル時間を短縮します(また、プライベート実装をより適切に非表示にする)。

例えば:

class X
{
private:
  C c;
  D d;  
} ;

次のように変更できます:

class X
{
private:
  struct XImpl;
  XImpl* pImpl;       
};

CPPでの定義:

struct X::XImpl
{
  C c;
  D d;
};

これはかなり興味深いようですが、これまでこの種のアプローチを見たことがありません。これまでに取り組んだ企業や、ソースコードを見たオープンソースプロジェクトでも見たことがありません。それで、このテクニックが実際に実際に使用されているのだろうか?

どこでも使用できますか、それとも注意して使用しますか?また、この手法は、パフォーマンスが非常に重要な組み込みシステムでの使用をお勧めしますか?


これは、Xが(抽象的な)インターフェイスであり、Ximplが実装であると決定することと本質的に同じですか?struct XImpl : public X。それは私にとってより自然に感じます。私が見逃した他の問題はありますか?
アーロン・マクデイド2012年

@AaronMcDaid:似ていますが、(a)メンバー関数が仮想である必要がない、(b)ファクトリー、または実装クラスの定義をインスタンス化する必要がないという利点があります。
Mike Seymour

2
@AaronMcDaid pimplイディオムは仮想関数呼び出しを回避します。また、C ++っぽいものもあります(C ++っぽい概念の場合)。ファクトリ関数ではなくコンストラクタを呼び出します。既存のコードベースの内容に応じて、両方を使用しました--- Pimplのイディオム(元々はCheshire catのイディオムと呼ばれ、Herbによる説明の少なくとも5年前のもの)はより長い歴史を持ち、 C ++で広く使用されていますが、それ以外の場合は両方とも機能します。
James

30
C ++では、pimplはでconst unique_ptr<XImpl>はなくで実装する必要がありXImpl*ます。
Neil G

1
「これまでにこの種のアプローチを見たことはありません。私が働いた会社でも、オープンソースプロジェクトでもそうではありません」。Qtはほとんど使用していません。
ManuelSchneid3r 2015

回答:


132

それで、このテクニックが実際に実際に使用されているのだろうか?どこでも使用できますか、それとも注意して使用しますか?

もちろん使用しています。私のプロジェクトでは、ほとんどすべてのクラスで使用しています。


PIMPLイディオムを使用する理由:

バイナリ互換性

ライブラリを開発しているときXImpl、クライアントとのバイナリ互換性を壊すことなくフィールドを追加/変更できます(これはクラッシュを意味します!)。Xクラスに新しいフィールドを追加しても、クラスのバイナリレイアウトは変更されないためXimpl、マイナーバージョンの更新でライブラリに新しい機能を追加しても安全です。

もちろん、バイナリの互換性を損なうことなく、新しいパブリック/プライベート非仮想メソッドをX/に追加することもできますがXImpl、これは標準のヘッダー/実装手法と同等です。

データ非表示

ライブラリ、特にプロプライエタリなライブラリを開発している場合は、ライブラリのパブリックインターフェイスを実装するために使用された他のライブラリ/実装手法を開示しないことが望ましい場合があります。知的所有権の問題のためか、ユーザーが実装について危険な仮定をしたり、ひどいキャストトリックを使用してカプセル化を壊したりする可能性があると考えているためです。PIMPLはそれを解決/軽減します。

コンパイル時間

XフィールドまたはメソッドXImpl(あるいはその両方)をクラスに追加/削除するときに、標準の手法でプライベートフィールド/メソッドの追加にマップするときに、ソース(実装)ファイルのみを再構築する必要があるため、コンパイル時間が短縮されます。実際には、これは一般的な操作です。

標準ヘッダー/実装技術(PIMPLを使用しない)では、新しいフィールドをに追加すると、割り当てのサイズを調整する必要があるため、(スタックまたはヒープのいずれかで)X割り当てるすべてのクライアントをX再コンパイルする必要があります。まあ、Xを割り当てないすべてのクライアント再コンパイルする必要がありますが、それはオーバーヘッドです(クライアント側の結果のコードは同じになります)。

さらに、カプセル化の理由でこのメソッドを呼び出すことができない場合でもXClient1.cpp、プライベートメソッドX::foo()が追加XおよびX.h変更された場合でも、標準のヘッダーと実装の分離を再コンパイルする必要がXClient1.cppあります。上記のように、これは純粋なオーバーヘッドであり、実際のC ++ビルドシステムの動作に関連しています。

もちろん、メソッドの実装を変更するだけの場合(ヘッダーに触れないため)再コンパイルは必要ありませんが、標準のヘッダー/実装手法と同等です。


この手法は、(パフォーマンスが非常に重要な)組み込みシステムでの使用をお勧めしますか?

それはあなたのターゲットがどれほど強力であるかに依存します。ただし、この質問への唯一の答えは、次のとおりです。あなたが得るものと失うものを測定および評価します。また、クライアントが組み込みシステムで使用するためのライブラリを公開していない場合は、コンパイル時の利点のみが適用されることを考慮してください。


16
+1は、私が働いている会社でも同じ理由で広く使用されているためです。
Benoit

9
また、バイナリ互換性
Ambroz Bizjak 2012年

9
Qtライブラリでは、このメソッドはスマートポインターの状況でも使用されます。したがって、QStringはその内容を内部的に不変クラスとして保持します。パブリッククラスが「コピー」されると、プライベートクラス全体ではなく、プライベートメンバーのポインタがコピーされます。これらのプライベートクラスもスマートポインターを使用するので、基本的にほとんどのクラスでガベージコレクションが行われます。また、クラス全体のコピーではなくポインターのコピーによるパフォーマンスの大幅な向上
Timothy Baldridge

8
さらに、pimplイディオムを使用すると、Qtは(ほとんどの場合)1つのメジャーバージョン内でバイナリの前方互換性と後方互換性の両方を維持できます。IMOこれは、使用する最も重要な理由です。
ホワイトクォーク2012年

1
同じAPIを保持できるため、プラットフォーム固有のコードの実装にも役立ちます。
2017年

49

少なくとも一部のバージョンでは、多くのライブラリがAPIを安定させるためにそれを使用しているようです。

しかし、すべてのものに関して、あなたは注意せずにどこでも何も使用するべきではありません。それを使用する前に常に考えます。それがあなたに与える利点を評価し、それらがあなたが支払う価格の価値があるかどうかを評価します。

それあなたに与えるかもしれない利点は:

  • 共有ライブラリのバイナリ互換性を維持するのに役立ちます
  • 特定の内部の詳細を隠す
  • 再コンパイルサイクルの減少

それらはあなたにとって本当の利点かもしれませんし、そうでないかもしれません。私と同じように、私は数分の再コンパイル時間を気にしません。エンドユーザーは通常、一度も最初からコンパイルするので、通常は行いません。

考えられる欠点は次のとおりです(ここでも、実装、およびそれらが実際の欠点であるかどうかによって異なります)。

  • 単純なバリアントよりも多くの割り当てによるメモリ使用量の増加
  • メンテナンス作業の増加(少なくとも転送機能を作成する必要があります)
  • パフォーマンスの低下(クラスの単純な実装の場合とは異なり、コンパイラーはインライン化できない場合があります)

そのため、すべてに慎重に値を設定し、自分で評価してください。私にとって、ほとんどの場合、pimplイディオムを使用することは努力に値しないことがわかります。私がそれを個人的に使用するケースは1つだけです(または少なくとも同様のもの)。

Linux stat呼び出し用のC ++ラッパー。ここで、Cヘッダーの構造体は、#defines設定内容に応じて異なる場合があります。そして、私のラッパーヘッダーはそれらのすべてを制御することはできないので、私#include <sys/stat.h>は自分の.cxxファイルでのみこれらの問題を回避しています。


2
ほとんどの場合、システムインターフェイスに使用して、インターフェイスコードをシステムから独立させる必要があります。たとえば、私のFileクラス(statUnixで返される情報の多くを公開します)は、WindowsとUnixの両方で同じインターフェイスを使用します。
James Kanze

5
@JamesKanze:そこにいても、私は最初に少しの間座っ#ifdefて、ラッパーをできるだけ薄くするために数秒では十分でないかどうか考えます。しかし、誰もが異なる目標を持っています。重要なことは、盲目的に何かに従うのではなく、時間をかけてそれについて考えることです。
PlasmaHH 2012年

31

商品については他のすべての意見に同意しますが、証拠を提示しておきます。テンプレートではうまく機能しません

その理由は、テンプレートのインスタンス化には、インスタンス化が行われた場所で利用可能な完全な宣言が必要だからです。(そして、それがCPPファイルに定義されたテンプレートメソッドが表示されない主な理由です)

テンプル化されたサブクラスを参照することはできますが、それらすべてを含める必要があるため、コンパイルにおける「実装デカップリング」(すべてのプラットフォーム固有のコードをすべての場所に含めることを避け、コンパイルを短縮する)の利点はすべて失われます。

古典的なOOP(継承ベース)には良いパラダイムですが、一般的なプログラミング(専門化ベース)には適していません。


4
より正確にする必要があります。PIMPLクラスをテンプレート型の引数として使用しても、まったく問題はありません。実装クラス自体を外部クラスのテンプレート引数でパラメーター化する必要がある場合のみ、それがまだプライベートクラスであっても、インターフェイスヘッダーから非表示にすることはできません。テンプレート引数を削除できる場合でも、確かに「適切な」PIMPLを実行できます。タイプの削除を使用すると、ベースの非テンプレートクラスでPIMPLを実行し、テンプレートクラスをそのクラスから派生させることもできます。
モニカ

22

他の人々はすでに技術的なメリット/デメリットを提供していますが、以下は注目に値すると思います:

何よりもまず、独断的ではありません。pImplがあなたの状況で機能する場合、それを使用してください-「それは本当に実装を隠しているのでそれ OOの方が良い」などという理由だけでそれを使用しないでください。C++ FAQの引用:

カプセル化は、人ではなくコード用です(ソース

使用するオープンソースソフトウェアの例とその理由を説明するために、OpenSceneGraphで使用されるスレッドライブラリであるOpenThreadsについて説明します。<Thread.h>内部状態変数(スレッドハンドルなど)はプラットフォームごとに異なるため、主なアイデアは、ヘッダー(たとえば)からすべてのプラットフォーム固有のコードを削除することです。この方法では、すべてが隠されているため、他のプラットフォームの特異性を知らなくても、ライブラリに対してコードをコンパイルできます。


12

他のモジュールがAPIとして使用するために公開されているクラスのPIMPLを主に検討します。これには、PIMPL実装で行われた変更の再コンパイルがプロジェクトの他の部分に影響を与えないため、多くの利点があります。また、APIクラスの場合、バイナリ互換性を促進します(モジュール実装の変更はそれらのモジュールのクライアントに影響を与えません。新しい実装が同じバイナリインターフェイス(PIMPLによって公開されるインターフェイス)を持っているため、再コンパイルする必要はありません)。

すべてのクラスでPIMPLを使用する場合は、これらの利点がすべて犠牲になるため、注意が必要です。実装メソッドにアクセスするには、追加のレベルの間接参照が必要です。


「実装メソッドにアクセスするには、追加の間接レベルが必要です。」そうですか?
xaxxon 2017年

@xaxxonはい、そうです。メソッドが低レベルの場合、pimplは遅くなります。たとえば、タイトなループで動作するものには決して使用しないでください。
Erik Aronesty 2018

@xaxxon一般的には、追加のレベルが必要です。インライン化が行われる場合は、いいえ。しかし、インライン化は、別のdllでコンパイルされたコードのオプションにはなりません。
ジータ2018

5

これはデカップリングの最も基本的なツールの1つだと思います。

埋め込みプロジェクト(SetTopBox)でpimpl(およびExceptional C ++の他の多くのイディオム)を使用していました。

このイディムのプロジェクトでの特別な目的は、XImplクラスが使用するタイプを非表示にすることでした。具体的には、これを使用して、さまざまなヘッダーがプルされるさまざまなハードウェアの実装の詳細を非表示にしました。あるプラットフォームと別のプラットフォームでは、XImplクラスの実装が異なりました。クラスXのレイアウトは、プラットフォームに関係なく同じままでした。


4

以前はこのテクニックをよく使用していましたが、その後、このテクニックから遠ざかっていました。

もちろん、実装の詳細をクラスのユーザーから隠すことは良い考えです。ただし、クラスのユーザーに抽象インターフェースを使用させ、実装の詳細を具象クラスにすることで、これを行うこともできます。

pImplの利点は次のとおりです。

  1. このインターフェースの実装が1つしかないと仮定すると、抽象クラス/具象実装を使用しない方が明確です。

  2. 複数のクラスが同じ「impl」にアクセスするようなクラスのスイート(モジュール)があるが、モジュールのユーザーは「公開された」クラスのみを使用する場合。

  3. これが悪いことだと思われる場合は、vテーブルはありません。

私が見つけたpImplの欠点(抽象インターフェースがよりうまく機能する場合)

  1. 「プロダクション」実装は1つしかないかもしれませんが、抽象インターフェースを使用することで、単体テストで機能する「模擬」実装を作成することもできます。

  2. (最大の問題)。unique_ptrと移動の時代の前は、pImplを保存する方法に関して選択の制限がありました。生のポインタとあなたのクラスがコピー不可能であるという問題がありました。古いauto_ptrは、順方向に宣言されたクラスでは機能しません(とにかくすべてのコンパイラでは機能しません)。したがって、人々はクラスをコピー可能にするのに優れたshared_ptrの使用を開始しましたが、もちろん両方のコピーの基になるshared_ptrは同じではなく、予想外の可能性があります(一方を変更すると、両方が変更されます)。そのため、解決策は、多くの場合、内部ポインターに未加工のポインターを使用し、クラスをコピー不可にして、代わりにshared_ptrを返すことでした。したがって、newへの2つの呼び出し。(実際には、3つの古いshared_ptrが2番目のものを提供します)。

  3. constnessはメンバーポインターに伝達されないため、技術的には実際にはconst-correctではありません。

そのため、一般的に私は長年、pImplから抽象インターフェースの使用法(およびインスタンスを作成するファクトリメソッド)に移行してきました。


3

他の多くの人が言ったように、Pimplイディオムは、残念ながらパフォーマンスの損失(追加のポインターの間接化)と追加のメモリーの必要性(メンバーポインター自体)により、完全な情報の非表示とコンパイルの独立性に到達できます。組み込みソフトウェアの開発、特にメモリを可能な限り節約する必要があるシナリオでは、追加のコストが重要になる可能性があります。C ++抽象クラスをインターフェイスとして使用すると、同じコストで同じ利点が得られます。これは実際にはC ++の大きな欠点を示しています。Cのようなインターフェイス(パラメーターとして不透明なポインターを使用するグローバルメソッド)を繰り返さないと、追加のリソースの欠点がなければ、真の情報の非表示とコンパイルの独立性を保つことができません。これは主に、ユーザーによるインクルードが必要なクラスの宣言、


3

これは私が遭遇した実際のシナリオで、このイディオムは非常に役立ちました。私は最近、ゲームエンジンでDirectX 11と既存のDirectX 9サポートをサポートすることを決定しました。エンジンはすでにほとんどのDX機能をラップしているため、DXインターフェイスは直接使用されませんでした。それらはヘッダーでプライベートメンバーとして定義されただけです。エンジンはDLLを拡張機能として利用し、キーボード、マウス、ジョイスティック、スクリプトのサポートを、他の多くの拡張機能と同じように追加します。これらのDLLのほとんどはDXを直接使用しませんでしたが、DXを公開するヘッダーをプルしただけなので、知識とDXへのリンクが必要でした。DX 11を追加すると、この複雑さは劇的に増加しますが、不必要です。ソースでのみ定義されたPimplにDXメンバーを移動すると、この面倒がなくなりました。このライブラリの依存関係の削減に加えて、


2

多くのプロジェクトで実際に使用されています。その有用性はプロジェクトの種類に大きく依存します。これを使用した最も顕著なプロジェクトの1つはQtです。基本的な考え方は、実装またはプラットフォーム固有のコードをユーザー(Qtを使用する他の開発者)から隠すことです。

これは高貴なアイデアですが、これには本当の欠点があります。デバッグプライベートな実装に隠されたコードが高品質である限り、これで十分ですが、そこにバグがある場合、ユーザー/開発者に問題があります。なぜなら、たとえ彼が実装のソースコードを持っているとしても、それは隠された実装への単なるばかげたポインタだからです。

したがって、ほとんどすべての設計決定と同様に、賛否両論があります。


9
それはばかげていますが、型付けされています...なぜデバッガでコードを追跡できないのですか?
UncleZeiv

2
一般的に言えば、Qtコードにデバッグするには、自分でQtをビルドする必要があります。一度実行すれば、PIMPLメソッドにステップインして、PIMPLデータの内容を検査することに問題はありません。
モニカを

0

私が見ることができる1つの利点は、プログラマが特定の操作をかなり高速に実装できることです。

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS:移動のセマンティクスを誤解していないことを願っています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.