web-dev-qa-db-ja.com

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

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

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

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

純粋仮想クラスは、pimplイディオムほど望ましくないのはなぜですか?

115
Arkaitz Jimenez

C++クラスを作成するときは、次のようになるかどうかを考えるのが適切です。

  1. 値タイプ

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

  2. エンティティタイプ

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

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

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

59

Pointer to implementationは通常、構造的な実装の詳細を隠すことです。 Interfacesは、異なる実装のインスタンス化に関するものです。彼らは本当に2つの異なる目的を果たします。

33
D.Shawley

Pimplイディオムは、特に大規模なアプリケーションでビルドの依存関係と時間を削減し、クラスの実装の詳細がヘッダーを1つのコンパイルユニットに公開するのを最小限に抑えます。クラスのユーザーは、にきびの存在を意識する必要さえありません(彼らが秘密ではない不可解なポインターを除きます!)。

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

27
Pontus Gagge

私は同じ質問の答えを探していました。いくつかの記事といくつかの実践を読んだ後「純粋な仮想クラスインターフェイス」の使用を好む

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

唯一の欠点(私はこれについて調査しようとしています

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

共有ライブラリには非常に現実的な問題があります。これは、純粋な仮想マシンではできないという単純なイディオムによってうまく回避されます。状況によっては許容される場合がありますが、システムライブラリ用。

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

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

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

コンパイラは、this

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

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

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

// 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())
{}

ここで必要なのは、実装オブジェクトへのポインタ以外のデータメンバーがパブリックインターフェイスにないようにすることです。このクラスのエラーから安全です。

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

8
unwesen

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

8
Masha Ananieva

継承は委任よりも強力で密接な結合であることを忘れてはなりません。また、特定の問題を解決するためにどのデザインイディオムを採用するかを決定する際に、回答で提起されたすべての問題を考慮します。

6
Sam

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

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

// 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();
3
Superfly Jon

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

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

リンゴとオレンジは本当に。

2

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

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

2
Trantor