web-dev-qa-db-ja.com

実際のPimplIdiom

SOpimplイディオムについていくつか質問がありましたが、どれくらいの頻度であるかについてもっと興味があります実際に活用されています。

パフォーマンスとカプセル化の間にはいくつかのトレードオフがあることを理解しています。さらに、余分なリダイレクトによるデバッグの煩わしさもあります。

それで、これはクラスごとに採用されるべきものですか、それともオールオアナッシングベースですか?これはベストプラクティスですか、それとも個人的な好みですか?

それはやや主観的だと思いますので、私の最優先事項を挙げさせてください。

  • コードの明確さ
  • コードの保守性
  • パフォーマンス

ある時点でコードをライブラリとして公開する必要があると常に想定しているので、それも考慮事項です。

編集:同じことを達成するための他のオプションは歓迎すべき提案です。

46
Ryan Emerle

クラスごとに行うのか、オールオアナッシングベースで行うのかは、そもそもなぜ単純なイディオムを選ぶのかによって決まると思います。ライブラリを構築するときの私の理由は、次のいずれかです。

  • 情報の開示を避けるために実装を隠したかった(はい、それはFOSSプロジェクトではありませんでした:)
  • クライアントコードへの依存度を下げるために、実装を非表示にしたかった。共有ライブラリ(DLL)を構築する場合は、アプリケーションを再コンパイルしなくても、pimplクラスを変更できます。
  • ライブラリを使用してクラスをコンパイルするのにかかる時間を短縮したかった。
  • 名前空間の衝突(または同様のもの)を修正したかった。

これらの理由のいずれも、オールオアナッシングアプローチを促すものではありません。最初のケースでは、非表示にしたいものを単純化するだけですが、2番目のケースでは、変更が予想されるクラスに対しては、おそらくそれで十分です。また、3番目と4番目の理由から、重要なメンバーを非表示にすることによるメリットしかありません。そのメンバーは、追加のヘッダー(たとえば、サードパーティのライブラリやSTL)を必要とします。

いずれにせよ、私のポイントは、私は通常、このようなものがあまり有用であるとは思わないということです。

class Point {
  public:      
    Point(double x, double y);
    Point(const Point& src);
    ~Point();
    Point& operator= (const Point& rhs);

    void setX(double x);
    void setY(double y);
    double getX() const;
    double getY() const;

  private:
    class PointImpl;
    PointImpl* pimpl;
}

この種の場合、ポインターを逆参照する必要があり、メソッドをインライン化できないため、トレードオフが発生し始めます。ただし、重要なクラスに対してのみ実行する場合は、通常、わずかなオーバーヘッドは問題なく許容できます。

35
Reunanen

Pimpl ideomの最大の用途の1つは、安定したC++ ABIの作成です。ほぼevery Qtクラスは、一種の単純な「D」ポインターを使用します。これにより、ABIを壊すことなく、はるかに簡単な変更を実行できます。

22
Artyom

コードの明確さ

コードの明確さは非常に主観的ですが、私の意見では、単一のデータメンバーを持つヘッダーは、多くのデータメンバーを持つヘッダーよりもはるかに読みやすくなっています。ただし、実装ファイルはノイズが多いため、明確さが低下します。クラスが基本クラスであり、ほとんどが維持されるのではなく派生クラスによって使用される場合、これは問題にならない可能性があります。

保守性

単純化されたクラスの保守性のために、私は個人的に、データメンバーの各アクセスで余分な逆参照が面倒だと感じています。データが純粋にプライベートである場合、アクセサーは役に立ちません。とにかく、アクセサーまたはミューテーターを公開するべきではなく、常にpimplを逆参照することに固執しているからです。

派生クラスの保守性については、ヘッダーファイルにリストされている無関係な詳細が少ないため、このイディオムはすべての場合において純粋な勝利であることがわかります。すべてのクライアントコンパイルユニットのコンパイル時間も改善されています。

パフォーマンス

多くの場合、パフォーマンスの低下は小さく、重大なものはほとんどありません。長期的には、仮想関数のパフォーマンス低下の大きさのオーダーです。データメンバーごとのアクセスごとの追加の逆参照、pimplの動的メモリ割り当て、および破棄時のメモリの解放について話します。 pimpl'dクラスがそのデータメンバーに頻繁にアクセスしない場合、pimpl'dクラス 'オブジェクトは頻繁に作成され、短命であるため、動的割り当てが余分な間接参照を上回る可能性があります。

決定

1つの追加の逆参照やメモリ割り当てが大きな違いを生むなど、パフォーマンスが重要なクラスでは、何があってもpimplを使用すべきではないと思います。このパフォーマンスの低下が重要でなく、ヘッダーファイルが広く#includeされている基本クラスでは、コンパイル時間が大幅に改善された場合、おそらくpimplを使用する必要があります。コンパイル時間が短縮されない場合、それはコードの明確さの好みにかかっています。

他のすべての場合、それは純粋に好みの問題です。決定を下す前に、試して実行時のパフォーマンスとコンパイル時のパフォーマンスを測定してください。

11
wilhelmtell

pImplは、強力な例外保証付きでstd :: swapとoperator =を実装する場合に非常に役立ちます。あなたのクラスがそれらのいずれかをサポートし、複数の重要なフィールドを持っている場合、それは通常、もはや好みに任されていないと言いたいです。

それ以外の場合は、ヘッダーファイルを介してクライアントを実装にどの程度緊密にバインドするかが重要です。バイナリ互換性のない変更が問題にならない場合は、保守性にあまりメリットがない可能性がありますが、コンパイル速度が問題になると、通常は節約できます。

パフォーマンスコストは、おそらく間接参照よりもインライン化の喪失に関係していますが、それは大げさな推測です。

後でいつでもpImplを追加でき、プライベートフィールドを追加したからといって、この日からクライアントを再コンパイルする必要がないことを宣言できます。

したがって、これはいずれも、オールオアナッシングアプローチを示唆するものではありません。あなたはそれがあなたに利益をもたらすクラスのためにそれを選択的に行うことができます、それがそうでないもののためではなく、そして後であなたの考えを変えることができます。たとえば、イテレータをpImplとして実装すると、デザインが多すぎるように聞こえます。

7
Steve Jessop

このイディオムは、大規模なプロジェクトのコンパイル時間に大いに役立ちます。

外部リンク

これもいいです

4
GregC

私は通常、ヘッダーファイルがコードベースを汚染するのを避けたいときに使用します。 Windows.hは完璧な例です。それはとても悪い振る舞いです、私はそれをどこにでも見えるようにするよりもむしろ自分自身を殺したいと思います。したがって、クラスベースのAPIが必要であると仮定すると、それをpimplクラスの背後に隠すと、問題が適切に解決されます。 (個々の関数を公開するだけで満足している場合は、もちろん、それらをpimplクラスに入れることなく、前方宣言することができます)

私はpimpl everywhereを使用しません。これは、パフォーマンスの低下と、通常は小さなメリットのために多くの余分な作業が必要なためです。それがあなたに与える主なものは、実装とインターフェースの間の分離です。通常、それはそれほど優先度が高くありません。

3
jalf

pImplは、r値のセマンティクスがある場合に最適に機能します。

PImplの「代替」は、実装の詳細を非表示にすることも可能にし、抽象基本クラスを使用して、実装を派生クラスに配置することです。ユーザーは、ある種の「ファクトリ」メソッドを呼び出してインスタンスを作成し、通常、抽象クラスへのポインタ(おそらく共有されているもの)を使用します。

代わりに、pImplの背後にある理論的根拠は次のとおりです。

  • Vテーブルに保存します。はい。ただし、コンパイラはすべての転送をインライン化し、実際に何かを保存します。
  • モジュールに、お互いを詳細に知っている複数のクラスが含まれている場合でも、外部にはそれを隠します。

PImplのコンテナクラスのセマンティクスは次のようになります。-コピー不可、割り当て不可...したがって、構築時にpImplを「新規」にし、破棄時に「削除」します-共有。つまり、Impl *ではなくshared_ptrがあります

Shared_ptrを使用すると、クラスがデストラクタのポイントで完了している限り、前方宣言を使用できます。デフォルトであっても、デストラクタを定義する必要があります(おそらくそうなるでしょう)。

  • 交換可能。 「空の可能性があります」と「スワップ」を実装できます。ユーザーは、1つのインスタンスを作成し、それに非const参照を渡して、「スワップ」を使用してインスタンスを設定できます。

  • 2段階の構造。空のものを作成し、その上で「load()」を呼び出してデータを入力します。

共有は、r値のセマンティクスなしで私がリモートでさえ好きな唯一のものです。それらを使用して、コピー不可、割り当て不可を適切に実装することもできます。私は私にそれを与える関数を呼び出すことができるのが好きです。

ただし、実装が1つしかない場合でも、pImplよりも抽象基本クラスを使用する傾向があることがわかりました。

2
CashCow

私は自分のライブラリのいくつかの場所でイディオムを使用しています。どちらの場合も、実装からインターフェイスをきれいに分割しています。たとえば、.hファイルで完全に宣言されたXMLリーダークラスがあります。このクラスには、非公開の.hファイルと.cppファイルで宣言および定義されているRealXMLReaderクラスへのPIMPLがあります。 RealXMlReaderは、私が使用しているXMLパーサー(現在はExpat)の便利なラッパーです。

この配置により、すべてのクライアントコードを再コンパイルすることなく、将来Expatから別のXMLパーサーに変更できます(もちろん、再リンクする必要があります)。

コンパイル時のパフォーマンス上の理由からこれを行うのではなく、便宜上、これを行うことに注意してください。全体でPIMPLを使用しない限り、3つを超えるファイルを含むプロジェクトはコンパイルできないと主張するPIMPLファブナティクスがいくつかあります。これらの人々が実際の証拠を作成することはなく、「Latkos」と「指数関数的時間」について漠然と言及しているだけであることは注目に値します。

2
anon