私は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;
};
これはかなりおもしろそうですが、これまでこの種のアプローチを見たことはありません。私が働いた会社でも、ソースコードを見たオープンソースプロジェクトでも見たことはありません。だから、このテクニックは実際に実際に使用されているのだろうか?
どこでも、または注意して使用する必要がありますか?また、この手法は、組み込みシステム(パフォーマンスが非常に重要)での使用を推奨していますか?
だから、このテクニックは実際に実際に使用されているのだろうか?どこでも、または注意して使用する必要がありますか?
もちろん使用されます。私のプロジェクトのほぼすべてのクラスで使用しています。
ライブラリを開発しているとき、クライアントとのバイナリ互換性を損なうことなく、クラッシュを意味するXImpl
にフィールドを追加/変更できます。 X
クラスの新しいフィールドをXimpl
クラスに追加しても_ [$ var] _クラスのバイナリレイアウトは変わらないため、マイナーバージョンの更新でライブラリに新しい機能を追加しても安全です。
もちろん、バイナリの互換性を損なうことなく、X
/XImpl
に新しいパブリック/プライベート非仮想メソッドを追加することもできますが、これは標準のヘッダー/実装手法と同等です。
ライブラリ、特にプロプライエタリなライブラリを開発している場合は、ライブラリのパブリックインターフェイスを実装するために使用された他のライブラリ/実装手法を開示しないことが望ましい場合があります。知的財産の問題、またはユーザーが実装について危険な仮定をしたり、ひどいキャスティングトリックを使用してカプセル化を破ったりする可能性があると考えているためです。 PIMPLはそれを解決/緩和します。
X
クラス(プライベートフィールドの追加にマップする)にフィールドやメソッドを追加/削除するときに、XImpl
のソース(実装)ファイルのみを再構築する必要があるため、コンパイル時間が短縮されます。 /標準手法のメソッド)。実際には、これは一般的な操作です。
標準ヘッダー/実装手法(PIMPLなし)では、X
に新しいフィールドを追加するとき、X
(スタックまたはヒープのいずれか)を割り当てるすべてのクライアントを再コンパイルする必要があります、割り当てのサイズを調整する必要があるため。さて、Xを割り当てないすべてのクライアントalsoを再コンパイルする必要がありますが、それは単にオーバーヘッドです(クライアント側での結果のコードは同じです)。
さらに、プライベートヘッダーX::foo()
がX
に追加され、XClient1.cpp
が変更された場合でも、標準ヘッダー/実装の分離によりX.h
を再コンパイルする必要があります。ただし、XClient1.cpp
はカプセル化の理由でこのメソッドを呼び出すことはできません!上記のように、それは純粋なオーバーヘッドであり、実際のC++ビルドシステムの動作に関連しています。
もちろん、メソッドの実装を変更するだけの場合(ヘッダーに触れないため)、再コンパイルは必要ありませんが、これは標準のヘッダー/実装手法と同等です。
この手法は、組み込みシステム(パフォーマンスが非常に重要な場所)での使用を推奨していますか?
それはあなたのターゲットがどれだけ強力かによる。しかし、この質問への唯一の答えは、あなたが得失するものを測定し評価することです。また、クライアントが組み込みシステムで使用することを意図したライブラリを公開していない場合、コンパイル時間の利点のみが適用されることを考慮してください!
少なくとも一部のバージョンでは、多くのライブラリがAPIを安定させるために使用しているようです。
しかし、すべてのものについては、慎重にどこでも使用しないでください。それを使用する前に常に考えなさい。それがあなたに与える利点を評価し、それらがあなたが支払う価格に見合うかどうかを評価します。
mayの利点:
それらはあなたにとって本当の利点かもしれませんし、そうでないかもしれません。私と同じように、数分の再コンパイル時間は気にしません。エンドユーザーは通常、一度も一度もコンパイルしないため、そうしません。
考えられる欠点は次のとおりです(ここでも、実装およびそれらが実際の欠点であるかどうかによって異なります)。
したがって、すべてに値を慎重に与え、自分で評価してください。私にとって、ほとんどの場合、Pimplイディオムを使用することは努力する価値がないことがわかります。私が個人的に使用するケースは1つだけです(または、少なくとも似たようなもの)。
Linux stat
呼び出し用のC++ラッパー。ここで、Cヘッダーの構造体は、#defines
設定されています。そして、ラッパーヘッダーですべてを制御できないため、#include <sys/stat.h>
私の.cxx
ファイルし、これらの問題を回避します。
商品について他のすべての人に同意しますが、証拠に制限を付けさせてください:テンプレートではうまく機能しません。
その理由は、テンプレートのインスタンス化には、インスタンス化が行われた場所で利用可能な完全な宣言が必要だからです。 (それが、CPPファイルに定義されたテンプレートメソッドが表示されない主な理由です)
テンプル化されたサブクラスを引き続き参照できますが、それらをすべて含める必要があるため、コンパイル時の「実装の分離」の利点(すべてのプラットフォーム固有のコードを含めることを避け、コンパイルを短縮する)はすべて失われます。
古典的なOOP(継承ベース)には適したパラダイムですが、汎用プログラミング(特殊化ベース)には適していません。
他の人々はすでに技術的な長所/短所を提供していますが、以下は注目に値すると思います:
何よりもまず、独断的ではありません。あなたの状況でpImplが動作する場合は、それを使用します-「OO it itreally実装を隠します」など。C++ FAQを引用:
カプセル化は人ではなくコード用です( source )
それが使用されるオープンソースソフトウェアの例とその理由を示すために:OpenThreads、 OpenSceneGraph によって使用されるスレッドライブラリ。主なアイデアは、ヘッダーから削除することです(例:<Thread.h>
)内部状態変数(スレッドハンドルなど)はプラットフォームごとに異なるため、すべてのプラットフォーム固有のコード。このようにして、他のプラットフォームの特異性をまったく知らなくても、ライブラリに対してコードをコンパイルできます。すべてが隠されているからです。
私は主に、公開されたクラスのPIMPLが他のモジュールによってAPIとして使用されると考えます。これには多くの利点があります。PIMPL実装で行われた変更を再コンパイルしても、プロジェクトの残りの部分には影響しないからです。また、APIクラスの場合、バイナリ互換性を促進します(モジュール実装の変更はそれらのモジュールのクライアントに影響を与えません。新しい実装が同じバイナリインターフェイス(PIMPLによって公開されるインターフェイス)を持っているため、再コンパイルする必要はありません)。
すべてのクラスでPIMPLを使用する場合、これらの利点はすべて犠牲になるため、注意が必要です。実装メソッドにアクセスするには、余分なレベルの間接参照が必要です。
これは、デカップリングの最も基本的なツールの1つだと思います。
私は組み込みプロジェクト(SetTopBox)でpimpl(およびExceptional C++の他の多くのイディオム)を使用していました。
私たちのプロジェクトにおけるこのidoimの特定の目的は、XImplクラスが使用するタイプを隠すことでした。具体的には、さまざまなヘッダーが取り込まれるさまざまなハードウェアの実装の詳細を非表示にするために使用しました。一方のプラットフォームと他方のプラットフォームではXImplクラスの実装が異なりました。クラスXのレイアウトは、プラットフォームに関係なく同じままでした。
私は過去にこのテクニックをよく使用していましたが、その後、自分はそれから遠ざかっていました。
もちろん、実装の詳細をクラスのユーザーから隠すことをお勧めします。ただし、クラスのユーザーに抽象インターフェースを使用させ、実装の詳細を具象クラスにすることでそれを行うこともできます。
PImplの利点は次のとおりです。
このインターフェイスの実装が1つだけであると仮定すると、抽象クラス/具象実装を使用しないことで明確になります。
複数のクラスが同じ「impl」にアクセスするが、モジュールのユーザーが「exposed」クラスのみを使用するようなクラスのスイート(モジュール)がある場合。
これが悪いことであると想定される場合、vテーブルはありません。
PImplの短所(抽象インターフェイスの方が効果的)
「プロダクション」実装は1つだけですが、抽象インターフェースを使用することで、ユニットテストで機能する「モック」実装を作成することもできます。
(最大の問題)。 unique_ptrと移動の日前は、pImplの保存方法に関して選択肢が制限されていました。生のポインタで、クラスがコピーできないという問題がありました。古いauto_ptrは、前方宣言されたクラスでは動作しません(とにかくすべてのコンパイラで動作するわけではありません)。そのため、人々はクラスをコピー可能にするのに便利なshared_ptrを使用し始めましたが、もちろん両方のコピーは、あなたが予期しないかもしれない同じ基本的なshared_ptrを持っていました(一方を変更し、両方が変更されます)。そのため、解決策は多くの場合、内部ポインタに生のポインタを使用し、クラスをコピー不可にして、代わりにshared_ptrを返すことでした。したがって、newへの2つの呼び出し。 (実際には、古いshared_ptrを指定した3つが2番目の値を与えました)。
技術的には、constnessはメンバーポインターに伝播されないため、実際にはconst-correctではありません。
したがって、一般的に私はここ数年でpImplから離れて、代わりに抽象インターフェースの使用(およびインスタンスを作成するファクトリメソッド)に移行しました。
他の多くの人が言ったように、Pimplのイディオムでは、残念ながらパフォーマンスの損失(追加のポインター間接化)と追加のメモリニーズ(メンバーポインター自体)のコストで、完全な情報の隠蔽とコンパイルの独立性を実現できます。組み込みソフトウェア開発、特にメモリを可能な限り節約する必要があるシナリオでは、追加コストが重要になる場合があります。インターフェイスとしてC++抽象クラスを使用すると、同じコストで同じ利点が得られます。これは実際にはC++の大きな欠陥を示しており、Cのようなインターフェイス(不透明なポインターをパラメーターとして持つグローバルメソッド)を繰り返さなければ、追加のリソースの欠点なしに真の情報の隠蔽とコンパイルの独立性を得ることができません:これは主にユーザーが含める必要があるクラスの宣言は、ユーザーが必要とするクラスのインターフェース(パブリックメソッド)だけでなく、ユーザーが必要としない内部(プライベートメンバー)もエクスポートします。
このイディオムが大いに役立った、私が遭遇した実際のシナリオを以下に示します。ゲームエンジンで、DirectX 11と既存のDirectX 9サポートをサポートすることに最近決めました。エンジンはすでにほとんどのDX機能をラップしているため、DXインターフェイスは直接使用されませんでした。ヘッダーでプライベートメンバーとして定義されただけです。エンジンはDLLを拡張機能として利用し、キーボード、マウス、ジョイスティック、およびスクリプトサポートを他の多くの拡張機能と同様に追加します。これらのDLLのほとんどはDXを直接使用しませんでしたが、DXを公開するヘッダーを取得したという理由だけで、DXへの知識とリンクが必要でした。 DX 11を追加する際、この複雑さは劇的に増加しましたが、不必要に増加しました。ソースでのみ定義されたPimplにDXメンバーを移動すると、この面倒がなくなりました。このライブラリ依存関係の削減に加えて、プライベートメンバー関数をPimplに移動し、前面のインターフェイスのみを公開することで、公開されたインターフェイスがよりクリーンになりました。
多くのプロジェクトで実際に使用されています。有用性は、プロジェクトの種類に大きく依存します。これを使用したより顕著なプロジェクトの1つは、 Qt です。ここで、基本的な考え方は、ユーザー(Qtを使用する他の開発者)から実装またはプラットフォーム固有のコードを隠すことです。
これは高貴なアイデアですが、これには重大な欠点があります。デバッグプライベート実装に隠されたコードが高品質である限り、これはすべてうまくいきますが、そこにバグがある場合、ユーザー/開発者に問題があります。なぜなら、たとえ彼が実装ソースコードを持っていたとしても、それは隠された実装への単なる愚かなポインタだからです。
そのため、ほぼすべての設計決定において賛否両論があります。
私が見ることができる利点の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:移動のセマンティクスを誤解しないでください。