なぜ「PIMPL」というイディオムを使用する必要があるのでしょうか?[重複]
-
09-06-2019 - |
質問
この質問にはすでに答えがあります:
- pImpl イディオムは実際に実際に使用されていますか? 11 件の回答
バックグラウンダー:
の PIMPLイディオム (Pointer to IMPLementation) は、パブリック クラスがその一部であるライブラリの外では見ることができない構造体またはクラスをパブリック クラスがラップする実装隠蔽の手法です。
これにより、内部実装の詳細とデータがライブラリのユーザーから隠されます。
このイディオムを実装するとき、パブリック クラスのメソッド実装はライブラリにコンパイルされ、ユーザーにはヘッダー ファイルしかないため、パブリック クラスではなく、なぜパブリック メソッドを 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");
}
解決
- あなたが欲しいから
Purr()
のプライベートメンバーを使用できるようにするCatImpl
.Cat::Purr()
なしではそのようなアクセスは許可されませんfriend
宣言。 - そうすれば、責任を混同することがなくなるからです。1 つのクラスが実装し、1 つのクラスが転送します。
他のヒント
ほとんどの人はこれをハンドルボディイディオムと呼んでいると思います。James Coplien の著書『Advanced C++ Programming Styles and Idioms』 (アマゾンリンク)。としても知られています チェシャ猫 ルイス・キャロルのキャラクターは、笑いだけが残るまで消え去ってしまうからです。
サンプル コードは 2 セットのソース ファイルに分散する必要があります。この場合、Cat.h のみが製品に同梱されるファイルになります。
CatImpl.h は Cat.cpp に含まれており、CatImpl.cpp には CatImpl::Purr() の実装が含まれています。これは、製品を使用している一般の人々には表示されません。
基本的には、実装をできる限り覗き見から隠すという考えです。これは、顧客のコードがコンパイルされリンクされる API を介してアクセスされる一連のライブラリとして出荷される商用製品がある場合に最も役立ちます。
2000 年に IONAS Orbix 3.3 製品を書き直すことでこれを実現しました。
他の人が述べたように、彼の技術を使用すると、実装がオブジェクトのインターフェイスから完全に切り離されます。そうすれば、Purr() の実装を変更するだけであれば、Cat を使用するものをすべて再コンパイルする必要がなくなります。
この手法は、と呼ばれる方法論で使用されます。 契約による設計.
価値があるのは、実装をインターフェースから分離することです。これは通常、小規模なプロジェクトではあまり重要ではありません。ただし、大規模なプロジェクトやライブラリでは、これを使用してビルド時間を大幅に短縮できます。
の実装を考慮してください。 Cat
多くのヘッダーが含まれる場合があり、独自にコンパイルするのに時間がかかるテンプレートのメタプログラミングが含まれる場合があります。ただ使いたいだけのユーザーがなぜそうすべきなのか Cat
それをすべて含める必要がありますか?したがって、必要なファイルはすべて pimpl イディオムを使用して隠蔽されます (したがって、次の前方宣言が行われます)。 CatImpl
)、インターフェイスを使用しても、ユーザーにそれらを含めることは強制されません。
私は非線形最適化用のライブラリ (「厄介な数学がたくさんある」と読んでください) を開発しています。これはテンプレートに実装されているため、コードの大部分はヘッダーにあります。コンパイルには約 5 分かかります (まともなマルチコア CPU 上で)。それ以外の場合は空のヘッダーを解析するだけです。 .cpp
には約 1 分かかります。そのため、ライブラリを使用する人はコードをコンパイルするたびに数分待つ必要があり、開発が非常に困難になります。 退屈な. 。ただし、実装とヘッダーを非表示にすることで、即座にコンパイルされる単純なインターフェイス ファイルがインクルードされるだけになります。
これは、実装が他の企業によってコピーされないように保護することとは必ずしも関係がありません。アルゴリズムの内部動作がメンバー変数の定義から推測できない限り (推測できる場合は、そうなる可能性があります)、いずれにせよ、おそらく起こらないでしょう。おそらくそれほど複雑ではなく、そもそも保護する価値もありません)。
クラスで pimpl イディオムを使用する場合、パブリック クラスのヘッダー ファイルの変更を回避できます。
これにより、外部クラスのヘッダー ファイルを変更せずに、pimpl クラスにメソッドを追加/削除できます。#includes を pimpl に追加/削除することもできます。
外部クラスのヘッダー ファイルを変更する場合は、そのヘッダー ファイルを #include するすべてのものを再コンパイルする必要があります (ヘッダー ファイルのいずれかが含まれている場合は、それらを #include するすべてのものを再コンパイルする必要があります。以下同様)
通常、Owner クラス (この場合は Cat) のヘッダー内の Pimpl クラスへの参照は、ここで行ったように前方宣言のみになります。これにより、依存関係を大幅に減らすことができるためです。
たとえば、Pimpl クラスに (単なるポインターや参照ではなく) メンバーとして ComplicatedClass がある場合、使用する前に ComplicatedClass を完全に定義する必要があります。実際には、これは「ComplicatedClass.h」をインクルードすることを意味します (これには、ComplicatedClass が依存するすべてのものも間接的にインクルードされます)。これにより、単一のヘッダーフィルが大量のものを取り込むことになり、依存関係 (およびコンパイル時間) の管理に悪影響を及ぼします。
pimpl idion を使用するときは、Owner タイプ (ここでは Cat になります) のパブリック インターフェイスで使用されるものを #include するだけで済みます。これにより、ライブラリを使用する人々にとって状況が良くなり、人々がライブラリの内部の一部に依存していることを心配する必要がなくなります。つまり、誤って、または許可されていないことを実行したいために #define を行う必要がなくなるということです。ファイルを含める前に、private public を実行してください。
単純なクラスの場合、通常は Pimpl を使用する理由はありませんが、型が非常に大きい場合には、Pimpl が大きな助けになることがあります (特に長いビルド時間を回避する場合)。
まあ、私なら使わないだろうけど。もっと良い代替案があります:
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 イディオムよりもこれの方が好きです。
アスペクト指向プログラミングをエミュレートするために PIMPL イディオムを使用します。アスペクト指向プログラミングでは、メンバー関数の実行の前後に事前、事後、およびエラーのアスペクトが呼び出されます。
struct Omg{
void purr(){ cout<< "purr\n"; }
};
struct Lol{
Omg* omg;
/*...*/
void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } }
};
また、基本クラスへのポインターを使用して、多くのクラス間でさまざまな側面を共有します。
このアプローチの欠点は、ライブラリ ユーザーが実行されるすべての側面を考慮する必要があるにもかかわらず、自分のクラスしか見ていないことです。副作用についてはドキュメントを参照する必要があります。
cpp ファイル内に impl->Purr への呼び出しを配置すると、将来的にはヘッダー ファイルを変更することなく、まったく別のことができるようになります。おそらく来年には、代わりに呼び出すことができるヘルパー メソッドを発見し、それを直接呼び出し、impl->Purr をまったく使用しないようにコードを変更できるでしょう。(はい、実際の impl::Purr メソッドを更新することで同じことを達成することもできますが、その場合、次の関数を順番に呼び出すだけで何も実現しない追加の関数呼び出しが発生することになります)
また、ヘッダーには定義のみが含まれ、より明確な分離を実現する実装が含まれていないことも意味します。これがこのイディオムの要点です。
ここ数日間、初めての pimpl クラスを実装しました。これを使用して、Borland Builder のwinsock2.h などで発生していた問題を解決しました。構造体のアライメントがめちゃくちゃになっているようで、クラスのプライベートデータにソケットのものがあるため、それらの問題はヘッダーを含むcppファイルに広がっていました。
pimpl を使用することで、winsock2.h が 1 つの cpp ファイルにのみ含まれるようになり、問題に蓋をすることができ、また攻撃されることを心配する必要がなくなります。
元の質問に答えると、呼び出しを pimpl クラスに転送することで私が見つけた利点は、pimpl クラスが pimpl する前の元のクラスと同じであり、実装が 2 つにまたがらないことです。奇妙な形で授業。単に pimpl クラスに転送するために public を実装する方がはるかに明確です。
ノデット氏が言ったように、1 つのクラス、1 つの責任。
これが言及する価値のある違いかどうかはわかりませんが、...
独自の名前空間で実装し、ユーザーに表示されるコードのパブリック ラッパー/ライブラリ名前空間を持つことは可能でしょうか。
catlib::Cat::Purr(){ cat_->Purr(); }
cat::Cat::Purr(){
printf("purrrrrr");
}
このようにして、すべてのライブラリ コードで cat 名前空間を利用できるようになり、クラスをユーザーに公開する必要が生じた場合には、catlib 名前空間でラッパーを作成できます。
これは、pimpl のイディオムがどれほどよく知られているにもかかわらず、現実の生活ではあまり頻繁に登場しないことを物語っていると思います(例:オープンソースプロジェクトにおいて)。
「メリット」が誇張されすぎているのではないかとよく思います。はい、実装の詳細の一部をさらに非表示にすることもできますし、ヘッダーを変更せずに実装を変更することもできます。しかし、これらが実際には大きな利点であるかどうかは明らかではありません。
つまり、実装を次のようにする必要があるかどうかは明らかではありません。 それ 十分に隠されており、実際に実装のみを変更する人はおそらく非常にまれです。新しいメソッドを追加する必要がある場合は、すぐにヘッダーを変更する必要があります。