web-dev-qa-db-ja.com

C ++で同じ目的でインターフェイスを使用できる一方で、PImplパターンのポイントは何ですか?

C++でPImplイディオムを使用しているソースコードがたくさんあります。私は、その目的がプライベートデータ/タイプ/実装を非表示にすることであるため、依存関係を削除し、コンパイル時間とヘッダーインクルードの問題を減らすことができると思います。

ただし、C++のインターフェース/純粋抽象クラスにもこの機能があり、データ/タイプ/実装を非表示にするためにも使用できます。また、オブジェクトを作成するときに呼び出し元にインターフェースだけを見せるには、インターフェースのヘッダーでファクトリメソッドを宣言します。

比較は次のとおりです。

  1. コスト

    インターフェイスの方法のコストは低くなります。パブリックラッパー関数の実装void Bar::doWork() { return m_impl->doWork(); }を繰り返す必要がないため、インターフェイスで署名を定義するだけです。

  2. よく理解されている

    インターフェース技術は、すべてのC++開発者によく理解されています。

  3. パフォーマンス

    インターフェイス方法のパフォーマンスはPImplイディオムより悪くはありません。どちらも追加のメモリアクセスです。性能は同じだと思います。

以下は私の質問を説明するための疑似コードです:

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

同じ目的は、インターフェイスを使用して実装できます。

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

では、PImplのポイントは何ですか?

49
ZijingWu

まず、PImplは通常、非ポリモーフィッククラスに使用されます。そして、ポリモーフィッククラスにPImplがある場合、それは通常ポリモーフィックのままです。つまり、インターフェースを実装し、基本クラスの仮想メソッドなどをオーバーライドします。したがって、PImplのより単純な実装はnotインターフェースであり、メンバーを直接含む単純なクラスです!

PImplを使用する理由は3つあります。

  1. binaryインターフェイス(ABI)をプライベートメンバーから独立させる。依存コードを再コンパイルせずに共有ライブラリを更新することは可能ですが、binaryインターフェースが同じままである場合に限ります。ここで、非メンバー関数の追加と非仮想メンバー関数の追加を除いて、ヘッダーのほとんどすべての変更により、ABIが変更されます。 PImplイディオムは、プライベートメンバーの定義をソースに移動し、ABIをその定義から切り離します。参照 壊れやすいバイナリインターフェイスの問題

  2. ヘッダーが変更されると、それを含むすべてのソースを再コンパイルする必要があります。また、C++のコンパイルはかなり遅くなります。したがって、プライベートメンバーの定義をソースに移動することで、PImplイディオムは、ヘッダーでプルする必要のある依存関係を減らしてコンパイル時間を短縮し、依存関係を再コンパイルする必要がないため、変更後のコンパイル時間をさらに短縮します。 (わかりました、これは非表示の具象クラスを持つインターフェース+ファクトリー関数にも適用されます)。

  3. C++の多くのクラスでは、例外の安全性が重要な特性です。多くの場合、複数のクラスを1つに構成する必要があります。これにより、複数のメンバーでの操作中にスローした場合、どのメンバーも変更されないか、スローした場合に含まれているオブジェクトを保持する必要がある場合にメンバーの一貫性が失われる操作が発生します。一貫した。このような場合、PImplの新しいインスタンスを作成して操作を実装し、操作が成功したときにそれらをスワップします。

実際には、インターフェイスは実装の非表示のみに使用することもできますが、次の欠点があります。

  1. 非仮想メソッドを追加してもABIは壊れませんが、仮想メソッドを追加すると壊れます。したがって、インターフェイスではメソッドの追加がまったく許可されていませんが、PImplでは許可されています。

  2. インターフェースはポインター/参照を介してのみ使用できるため、ユーザーは適切なリソース管理を行う必要があります。一方、PImplを使用するクラスはまだ値型であり、リソースを内部的に処理します。

  3. 非表示の実装は継承できません。PImplを持つクラスは継承できます。

そしてもちろん、インターフェースは例外的な安全性には役立ちません。そのためには、クラス内の間接参照が必要です。

51
Jan Hudec

私はあなたのパフォーマンスのポイントに取り組みたいと思っています。インターフェースを使用する場合、コンパイラーのオプティマイザーによってインライン化されない仮想関数が作成されている必要があります。 PIMPL関数はインライン化することができます(非常に短いため、おそらくそうなるでしょう)インライン化できます(IMPL関数も小さい場合は、おそらく2回以上)。実行に非常に長い時間がかかる完全なプログラム分析最適化を使用しない限り、仮想関数呼び出しは最適化できません。

PIMPLクラスがパフォーマンス上重要な方法で使用されていない場合、この点はそれほど重要ではありませんが、パフォーマンスが同じであるという仮定は、すべてではなく一部の状況でのみ保持されます。

8
Chewy Gumball

このページ はあなたの質問に答えます。多くの人があなたの主張に同意します。 Pimplを使用すると、プライベートフィールド/メンバーよりも次の利点があります。

  1. クラスのプライベートメンバー変数を変更すると、そのクラスに依存するクラスを再コンパイルする必要がないため、作成時間が短縮され、 FragileBinaryInterfaceProblem が削減されます。
  2. ヘッダーファイルは、プライベートメンバー変数で「値によって」使用されるクラスを#includeする必要がないため、コンパイル時間が短縮されます。

Pimplイディオムは、コンパイル時の最適化であり、依存関係を解消する手法です。大きなヘッダーファイルを削減します。それはあなたのヘッダーをパブリックインターフェースのみに減らします。ヘッダーの長さは、C++ではどのように機能するかを考えるときに重要です。 #includeは効果的にヘッダーファイルをソースファイルに連結します。前処理されたC++ユニットが非常に大きくなる可能性があることを覚えておいてください。 Pimplを使用すると、コンパイル時間が短縮されます。

設計を改善することを含む他のより良い依存関係を壊す技術があります。プライベートメソッドは何を表していますか?単一の責任とは何ですか?彼らは別のクラスでなければなりませんか?

5
Dave Hillier