質問

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

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

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

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

役に立ちましたか?

解決

C ++クラスを作成するとき、それが行われるかどうかを考えるのが適切です

  1. 値の種類

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

  2. エンティティタイプ

    アイデンティティは重要です。常に参照によって渡され、「値」によって渡されることはありません。多くの場合、「コピー」する意味がありません。クラスのインスタンス。理にかなっている場合、多態的な「クローン」は通常、この方法がより適切です。例:Socketクラス、Databaseクラス、" policy"クラス、"閉鎖"関数型言語。

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

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

他のヒント

実装へのポインタは、通常、構造的な実装の詳細を隠すことです。 インターフェースは、異なる実装のインスタンス化に関するものです。それらは本当に2つの異なる目的に役立ちます。

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

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

同じ質問の答えを探していました。 いくつかの記事といくつかの練習を読んだ後、「純粋な仮想クラスインターフェイス」を使用することを好みます。

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

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

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

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

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

// 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)バイトのメモリを割り当て、次にそのメモリへのポインタを A :: A()コンストラクターを this として。

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

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

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

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

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

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

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

他の回答で広く取り上げられていますが、仮想ベースクラスに対する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つのことはまったく異なる目的に役立ちます。にきびイディオムの目的は、基本的に実装のハンドルを提供して、ソートの高速スワップなどを行えるようにすることです。

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

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

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

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

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top