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


49

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のポイントは何ですか?

回答:


50

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

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

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

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

  3. C ++例外の多くのクラスにとって、安全性は重要な特性です。多くの場合、複数のクラスを1つに構成して、操作中に複数のメンバーがスローされる場合、メンバーが変更されないようにするか、スローしてメンバーを不整合状態のままにする操作があり、包含オブジェクトを残す必要がある場合があります一貫しています。そのような場合、PImplの新しいインスタンスを作成して操作を実装し、操作が成功したときにそれらを交換します。

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

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

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

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

そしてもちろん、インターフェースは例外の安全性には役立ちません。そのためには、クラス内のインダイレクションが必要です。


いい視点ね。Implクラスにパブリック関数を追加してもが壊れませんABIが、インターフェイスにパブリック関数を追加しても壊れABIます。
-ZijingWu

1
パブリック関数/パブリックメソッドを追加しても、ABIを壊す必要はありません。厳密に相加的な変更は変更を壊しません。ただし、これほど注意する必要があります。フロントエンドとバックエンドを仲介するコードは、バージョン管理のあらゆる種類の楽しみに対処する必要があります。それはすべてトリッキーになります。(この種のことは、多くのソフトウェアプロジェクトがC ++よりもCを好む理由です。ABIが何であるかを正確に把握できるようになります。)
ドナルフェローズ

3
@DonalFellows:パブリック関数/メソッドを追加しても ABIが壊れません仮想メソッドの追加は、ユーザーがオブジェクトを継承できないようにしない限り(PImplで実現)、常に実行されます。
ヤンHudec

4
仮想メソッドを追加するとABIが壊れる理由を明確にする。それはリンケージに関係しています。ライブラリが静的か動的かに関係なく、非仮想メソッドへのリンクは名前によるものです。別の非仮想メソッドを追加すると、リンクする別の名前が追加されます。ただし、仮想メソッドはvtable内のインデックスであり、外部コードはインデックスによって効果的にリンクします。仮想メソッドを追加すると、外部コードが知ることなく、vtable内で他のメソッドを移動できます。
SnakE

1
PImplは、初期コンパイル時間も短縮します。たとえば、実装にunordered_setPImplを使用せずにテンプレートタイプ(たとえば)のフィールドがある場合、#include <unordered_set>in にする必要がありMyClass.hます。PIMPLでは、私だけでそれを含める必要は.cppなくファイル.h...ファイル、したがって、それを含んでいる他のすべて
マーティンC. Martin氏は

7

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

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


1
リンク時最適化を使用すると、pimplイディオムを使用することによるオーバーヘッドのほとんどが取り除かれます。外部ラッパー関数と実装関数の両方をインライン化して、ヘッダー内に実装された通常のクラスのように関数呼び出しを効果的に行うことができます。
-goji

これは、リンク時最適化を使用する場合にのみ当てはまります。PImplのポイントは、コンパイラーに実装がなく、転送機能さえないことです。しかし、リンカーはそうします。
マーティンC.マーティン

5

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

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

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

設計を改善することを含む、他のより良い依存関係破壊テクニックがあります。プライベートメソッドは何を表していますか?単一の責任とは何ですか?別のクラスにすべきですか?


PImplは、実行時の点で最適化されていません。割り当ては簡単ではなく、pimplは追加の割り当てを意味します。これは、依存関係を破る手法であり、コンパイル時間のみの最適化です。
Jan Hudec

@JanHudecが明確になりました。
デイブヒリアー

@DaveHillier、言及FragileBinaryInterfaceProblemの+1
ZijingWu
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.