PimplイディオムとPure仮想クラスインターフェイス


118

プログラマーがPimplイディオムまたは純粋な仮想クラスと継承のどちらを選択するのかを考えていました。

Pimplイディオムには、各パブリックメソッドとオブジェクト作成のオーバーヘッドごとに1つの明示的な間接指定が付属していることを理解しています。

一方、純粋仮想クラスには、継承する実装のための暗黙の間接指定(vtable)が付属しており、オブジェクト作成のオーバーヘッドがないことを理解しています。
編集:しかし、外部からオブジェクトを作成する場合は、ファクトリが必要です

純粋な仮想クラスがpimplイディオムより望ましくない理由は何ですか?


3
すばらしい質問です。同じことを聞きたかっただけです。boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Frank

回答:


60

C ++クラスを書くとき、それがなるかどうかを考えるのが適切です

  1. 値タイプ

    値でコピーし、アイデンティティは重要ではありません。std :: mapのキーであることが適切です。例:「文字列」クラス、「日付」クラス、または「複素数」クラス。そのようなクラスのインスタンスを「コピー」することには意味があります。

  2. エンティティタイプ

    アイデンティティは重要です。常に参照によって渡され、「値」によって渡されることはありません。多くの場合、クラスのインスタンスを「コピー」することはまったく意味がありません。それが理にかなっている場合、通常、ポリモーフィックな「クローン」メソッドがより適切です。例:ソケットクラス、データベースクラス、「ポリシー」クラス、関数型言語では「クロージャ」になるものすべて。

pImplと純粋な抽象基本クラスはどちらも、コンパイル時の依存関係を減らすための手法です。

ただし、pImplを使用して値の型(タイプ1)を実装することはありますが、結合とコンパイル時の依存関係を本当に最小限にしたい場合にのみです。多くの場合、それは気にする価値はありません。正しく指摘しているように、すべてのパブリックメソッドに対して転送メソッドを作成する必要があるため、構文上のオーバーヘッドが増えます。タイプ2のクラスの場合、私は常に、関連付けられたファクトリメソッドを持つ純粋な抽象基本クラスを使用します。


6
この回答に対する Paul de Vriezeのコメントをご覧ください。ライブラリを使用していて、クライアントを再構築せずに.so / .dllを交換したい場合、PimplとPure Virtualは大きく異なります。クライアントは名前でpimplフロントエンドにリンクするため、古いメソッドシグネチャを保持するだけで十分です。純粋な抽象ケースのOTOHは、vtableインデックスによって効果的にリンクするため、メソッドを並べ替えたり、途中に挿入したりすると、互換性が失われます。
SnakE 2015年

1
バイナリの互換性を維持するために、Pimplクラスのフロントエンドでのみメソッドを追加(または並べ替え)できます。論理的に言えば、あなたはまだインターフェースを変更しており、少しおかしなようです。ここでの答えは、「ディペンデンシーインジェクション」によるユニットテストにも役立つ可能性のある賢明なバランスです。しかし、答えは常に要件に依存します。サードパーティのライブラリ作成者(自分の組織でライブラリを使用する場合とは異なります)は、Pimplを非常に好むかもしれません。
Spacen Jasset

31

Pointer to implementation通常、構造実装の詳細を非表示にします。Interfaces異なる実装のインスタンス化についてです。それらは実際には2つの異なる目的を果たします。


13
必ずしもそうとは限りませんが、希望する実装に応じて複数のパイプを格納するクラスを見てきました。多くの場合、これは、プラットフォームごとに異なる方法で実装する必要がある何かのwin32実装とLinux実装の違いです。
Doug T.

14
しかし、インターフェイスを使用して実装の詳細を分離し、それらを非表示にすることができます
Arkaitz Jimenez

6
インターフェイスを使用してpimplを実装できますが、実装の詳細を分離する理由はしばしばありません。したがって、ポリモーフィックにする理由はありません。理由 PIMPLのためには、(ヘッダそれらを保つためにC ++で)離れたクライアントから実装の詳細を維持することです。あなたは抽象的なベース/インターフェースを使用してこれを行うかもしれませんが、一般的にそれは不必要なやり過ぎです。
マイケル・バー、

10
なぜそれはやり過ぎですか?つまり、インターフェイスメソッドは、pimplメソッドよりも遅いですか?そこ論理的な理由かもしれませんが、実用的な観点から、私は抽象インタフェースでそれを行うために、その簡単に言うだろう
Arkaitzヒメネス

1
私は抽象基底クラス/インターフェースが物事を行う「通常の」方法であり、
モック

28

pimplイディオムは、特に大きなアプリケーションでビルドの依存関係と時間を削減するのに役立ち、1つのコンパイル単位に対するクラスの実装の詳細のヘッダーの露出を最小限に抑えます。あなたのクラスのユーザーは、ニキビの存在に気づく必要さえありません(彼らが特権を持たない不可解なポインターとしては!)。

抽象クラス(純粋仮想)は、クライアントが知っておく必要のあるものです。それらを使用して結合と循環参照を削減しようとする場合、オブジェクトを作成できるようにする何らかの方法を追加する必要があります(たとえば、ファクトリメソッドまたはクラスを通じて、依存性注入または他のメカニズム)。


17

同じ質問の答えを探していました。いくつかの記事と実践読んだ後、「Pure virtual class interfaces」を使用することを好みます

  1. 彼らはもっと簡単です(これは主観的な意見です)。Pimplイディオムは、自分のコードを読み取る「次の開発者」ではなく、「コンパイラー用」のコードを書いているように感じさせます。
  2. 一部のテストフレームワークは、純粋な仮想クラスのモッキングを直接サポートしています。
  3. ファクトリーが外部からアクセスできる必要があるのは事実です。しかし、ポリモーフィズムを活用したい場合は、これも「プロ」であり、「コン」ではありません。...そして単純なファクトリーメソッドはそれほど痛くない

唯一の欠点(私はこれについて調査しようとしています)は、pimplイディオムがより高速になる可能性があることです

  1. プロキシ呼び出しがインライン化されている場合、継承では必ず実行時にオブジェクトVTABLEへの追加アクセスが必要
  2. pimpl public-proxy-classのメモリフットプリントが小さい(スワップを高速化するための最適化やその他の同様の最適化を簡単に行うことができます)

21
また、継承を使用すると、vtableレイアウトへの依存関係が発生することにも注意してください。ABIを維持するために、仮想関数を変更することはもうできません(独自の仮想メソッドを追加する子クラスがない場合、最後に追加することは安全です)。
ポールドブリーズ

1
^ここのこのコメントは粘着性があるはずです。
CodeAngry 14

10

ニキビ嫌い!彼らはクラスを醜くし、読みにくいです。すべてのメソッドはにきびにリダイレクトされます。ヘッダーには表示されず、どの機能にクラスがあるかを確認できないため、クラスをリファクタリングできません(たとえば、メソッドの可視性を変更するだけ)。クラスは「妊娠中」のように感じます。iterfacesを使用する方が、クライアントから実装を隠すのに十分であり、本当に十分だと思います。イベントを使用すると、1つのクラスに複数のインターフェースを実装させて、それらを薄くすることができます。インターフェースを好むはずです!注:ファクトリクラスは必要ありません。関連するのは、クラスクライアントが適切なインターフェースを介してそのインスタンスと通信することです。私が奇妙なパラノイアとして見つけたプライベートメソッドの非表示は、インターフェイスを持っているので、その理由はわかりません。


1
純粋な仮想インターフェースを使用できない場合があります。たとえば、いくつかのレガシーコードがあり、2つのモジュールがあり、それらに触れることなく分離する必要がある場合です。
AlexTheo 2014年

@Paul de Vriezeが以下に指摘するように、クラスのvtableに暗黙的に依存しているため、基本クラスのメソッドを変更すると、ABIの互換性が失われます。これが問題であるかどうかは、ユースケースによって異なります。
H. Rittich

「奇妙な偏執狂として私が見つけたプライベートメソッドの非表示」これにより、依存関係を非表示にして、依存関係が変更された場合にコンパイル時間を最小限に抑えることができませんか?
pooya13

また、ファクトリーがpImplよりもリファクタリングしやすいのもわかりません。どちらの場合も「インターフェース」を残して実装を変更しませんか?Factoryでは、1つの.hと1つの.cppファイルを変更する必要があります。pImplでは、1つの.hと2つの.cppファイルを変更する必要がありますが、それだけであり、通常、pImplのインターフェイスのcppファイルを変更する必要はありません。
pooya13

8

共有ライブラリには非常に現実的な問題があり、Pimplのイディオムは純粋な仮想ではそれをうまく回避できません。クラスのユーザーにコードの再コンパイルを強制せずに、クラスのデータメンバーを安全に変更/削除することはできません。これは、状況によっては許容できる場合もありますが、たとえばシステムライブラリの場合は許容できません。

問題を詳細に説明するために、共有ライブラリ/ヘッダーの次のコードを検討してください:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

コンパイラーは、初期化される整数のアドレスを、それが知っているAオブジェクトへのポインターからの特定のオフセット(この場合は唯一のメンバーであるため、おそらくゼロ)になるように初期化する整数のアドレスを計算する共有ライブラリー内のコードを発行しますthis

コードのユーザー側では、a new Aはまずsizeof(A)メモリのバイトを割り当て、次にそのメモリへのポインタをA::A()としてコンストラクタに渡しますthis

ライブラリの新しいリビジョンで整数を削除するか、整数を大きくするか、小さくするか、メンバーを追加する場合、ユーザーのコードが割り当てるメモリの量と、コンストラクターコードが予期するオフセットの間に不一致があります。運が良ければクラッシュの可能性があります。運が悪いと、ソフトウェアの動作がおかしくなります。

メモリ割り当てとコンストラクター呼び出しが共有ライブラリーで発生するため、データメンバーを内部クラスに安全に追加および削除できます。

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

ここで必要なことは、実装オブジェクトへのポインター以外のデータメンバーからパブリックインターフェイスを解放することだけであり、このクラスのエラーから安全です。

編集:ここでコンストラクタについて話している唯一の理由は、追加のコードを提供したくなかったということです-データメンバーにアクセスするすべての関数に同じ引数が適用されます。


4
:代わりに、void *型の、私はそれが前方に実装するクラスを宣言するために、より伝統的なことだと思うclass A_impl *impl_;
フランク・クルーガー

9
わかりません。インターフェイスとして使用する仮想純粋クラスでプライベートメンバーを宣言するべきではありません。アイデアは、クラスを非常に抽象的、サイズなし、純粋仮想メソッドのみに保つことです。何も表示されません。共有ライブラリを介して実行することはできません
Arkaitz Jimenez '05

@フランク・クルーガー:そうです、私は怠惰でした。@Arkaitz Jimenez:少し誤解。純粋な仮想関数のみを含むクラスがある場合、共有ライブラリについて説明してもあまり意味がありません。一方、共有ライブラリを扱っている場合は、上記で概説した理由により、パブリッククラスを実装するのが賢明です。

10
これは正しくありません。どちらの方法でも、他のクラスを「純粋な抽象ベース」クラスにした場合、クラスの実装状態を非表示にすることができます。
ポールホリングスワース

10
anserの最初の文は、ファクトリメソッドが関連付けられた純粋な仮想関数では、クラスの内部状態を非表示にできないことを意味しています。それは真実ではない。どちらの手法でも、クラスの内部状態を非表示にすることができます。違いは、それがユーザーにどのように見えるかです。pImplを使用すると、値のセマンティクスでクラスを表すことができますが、内部状態も非表示にすることができます。Pure Abstract Base Class +ファクトリメソッドを使用すると、エンティティタイプを表すことができ、内部状態を非表示にすることもできます。後者は、まさにCOMの仕組みです。「エッセンシャルCOM」の第1章には、これについての大きな議論があります。
ポールホリングスワース

6

継承は委任よりも強く、より密接な結合であることを忘れてはなりません。また、特定の問題を解決するために使用する設計のイディオムを決定するときに与えられた回答で提起されたすべての問題を考慮に入れます。


3

他の回答で広くカバーされていますが、仮想基底クラスに対するpimplの利点の1つについて、私はもう少し明確にすることができます。

pimplアプローチは、ユーザーの観点からは透過的です。つまり、たとえば、スタックにクラスのオブジェクトを作成し、コンテナーで直接使用できます。抽象仮想基本クラスを使用して実装を非表示にしようとすると、基本クラスへの共有ポインターをファクトリーから返す必要があり、使用が複雑になります。以下の同等のクライアントコードを検討してください。

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();

2

私の理解では、これら2つのことは完全に異なる目的を果たします。にきびのイディオムの目的は、基本的に実装へのハンドルを提供することで、ソートの高速スワップなどを実行できるようにすることです。

仮想クラスの目的は、ポリモーフィズムを許可するということです。つまり、派生型のオブジェクトへの不明なポインターがあり、関数xを呼び出すと、ベースポインターが実際に指すクラスに応じて常に適切な関数を取得します。

りんごとオレンジ。


私はリンゴ/オレンジに同意します。しかし、関数型にはpImplを使用しているようです。私の目標は主にビルド​​技術と情報隠蔽です。
xtofl 2009

2

pimplイディオムの最も厄介な問題は、既存のコードの保守と分析が非常に困難になることです。したがって、pimplを使用すると、「ビルドの依存関係と時間を削減し、実装の詳細がヘッダーに表示されるのを最小限に抑える」ためだけに、開発者の時間とフラストレーションを支払うことになります。それが本当に価値があるなら、あなた自身を決めなさい。

特に「ビルド時間」は、より優れたハードウェアまたはIncredibuild(www.incredibuild.com、Visual Studio 2017にも含まれている)などのツールを使用して解決できる問題であり、ソフトウェア設計には影響しません。ソフトウェアの設計は、通常、ソフトウェアの構築方法とは無関係である必要があります。


ビルド時間が2ではなく20分である場合は、開発者の時間も支払うため、少しバランスが取れているため、実際のモジュールシステムはここで非常に役立ちます。
Arkaitz Jimenez

私見、ソフトウェアの構築方法は、内部の設計にまったく影響を与えるべきではありません。これはまったく別の問題です。
Trantor 2018年

2
分析が難しいのはなぜですか?Implクラスに転送する実装ファイルでの一連の呼び出しは、難しく聞こえません。
マブラハム2018

2
pimplとインターフェースの両方が使用されるデバッグ実装を想像してみてください。ユーザーコードAの呼び出しから開始して、インターフェイスBにトレースし、実装されたクラスCにジャンプして、実装クラスDのデバッグを開始します...実際に何が起きているかを分析できるようになるまでの4つのステップ。そして、すべてがDLLに実装されている場合は、Cインターフェースがその中間のどこかにある可能性があります。
Trantor 2018

pImplがインターフェイスの機能も実行できるのに、なぜpImplでインターフェイスを使用するのですか?(つまり、依存関係の逆転を実現するのに役立ちます)
pooya13
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.