174

私はハーブサッターの「ExceptionalC++」という本を読んでいて、その本でPIMPLイディオムについて学びました。基本的に、アイデアは、のprivateオブジェクトの構造を作成し、classそれらを動的に割り当ててコンパイル時間を短縮することです(また、プライベート実装をより適切に非表示にします)。

例えば:

class X
{
private:
  C c;
  D d;
} ;

次のように変更できます:

class X
{
private:
  struct XImpl;
  XImpl* pImpl;
};

そして、.cppファイルでは、次の定義があります。

struct X::XImpl
{
  C c;
  D d;
};

これはかなり興味深いように思えますが、私が働いた会社でも、ソースコードを見たオープンソースプロジェクトでも、この種のアプローチを見たことがありません。それで、このテクニックが実際に実際に使われているのだろうか?

どこでも使用する必要がありますか、それとも注意して使用する必要がありますか?また、この手法は、組み込みシステム(パフォーマンスが非常に重要な場合)での使用が推奨されていますか?

4

12 に答える 12

141

それで、このテクニックが実際に実際に使われているのだろうか?どこでも使用する必要がありますか、それとも注意して使用する必要がありますか?

もちろん使用されます。私は自分のプロジェクトで、ほとんどすべてのクラスでそれを使用しています。


PIMPLイディオムを使用する理由:

バイナリ互換性

XImplライブラリを開発しているときは、クライアントとのバイナリ互換性を損なうことなく、フィールドを追加/変更できます(これはクラッシュを意味します!)。Xクラスに新しいフィールドを追加してもクラスのバイナリレイアウトは変更されないXimplため、マイナーバージョンの更新でライブラリに新しい機能を追加しても安全です。

もちろん、バイナリ互換性を損なうことなく、新しいパブリック/プライベート非仮想メソッドを追加することもできますが、これは標準のヘッダー/実装手法と同等ですXXImpl

データの非表示

ライブラリ、特にプロプライエタリライブラリを開発している場合は、ライブラリのパブリックインターフェイスを実装するために使用された他のライブラリ/実装手法を開示しないことが望ましい場合があります。知的財産の問題のため、またはユーザーが実装について危険な仮定をしたり、ひどいキャストトリックを使用してカプセル化を破ったりする誘惑に駆られる可能性があると考えているためです。PIMPLはそれを解決/軽減します。

コンパイル時間

Xフィールドやメソッドをクラスに追加/削除するときに、のソース(実装)ファイルのみを再構築する必要があるため、コンパイル時間が短縮されますXImpl(これは、標準的な手法でのプライベートフィールド/メソッドの追加にマップされます)。実際には、これは一般的な操作です。

標準のヘッダー/実装手法(PIMPLなし)では、に新しいフィールドを追加するときに、(スタックまたはヒープのいずれかで)X割り当てるすべてのクライアントを再コンパイルする必要があります。これは、割り当てのサイズを調整する必要があるためです。XXを割り当てないすべてのクライアント再コンパイルする必要がありますが、それは単なるオーバーヘッドです(クライアント側で結果として得られるコードは同じになります)。

さらに、カプセル化の理由でこのメソッドを呼び出すことができない場合でも、プライベートメソッドが追加および変更された場合でも、標準のヘッダー/実装の分離XClient1.cppを再コンパイルする必要があります。上記のように、これは純粋なオーバーヘッドであり、実際のC++ビルドシステムがどのように機能するかに関連しています。X::foo()XX.hXClient1.cpp

もちろん、メソッドの実装を変更するだけの場合(ヘッダーに触れないため)、再コンパイルは必要ありませんが、これは標準のヘッダー/実装手法と同等です。


この手法は、組み込みシステム(パフォーマンスが非常に重要な場合)での使用をお勧めしますか?

それはあなたのターゲットがどれほど強力かによります。ただし、この質問に対する唯一の答えは、何が得られ、何が失われるかを測定および評価することです。また、クライアントが組み込みシステムで使用することを目的としたライブラリを公開していない場合は、コンパイル時の利点のみが適用されることを考慮してください。

于 2012-01-23T13:56:37.790 に答える
52

少なくとも一部のバージョンでは、APIの安定性を維持するために多くのライブラリがこれを使用しているようです。

しかし、すべてのことに関して、あなたは注意なしにどこでも何も使うべきではありません。使用する前に必ず考えてください。それがあなたにどんな利点を与えるか、そしてそれらがあなたが支払う価格の価値があるかどうかを評価してください。

それがあなたに与えるかもしれない利点は次のとおりです:

  • 共有ライブラリのバイナリ互換性を維持するのに役立ちます
  • 特定の内部詳細を非表示にする
  • 再コンパイルサイクルの減少

それらはあなたにとって本当の利点かもしれないし、そうでないかもしれません。私のように、私は数分の再コンパイル時間を気にしません。エンドユーザーも通常はコンパイルしません。これは、エンドユーザーが常に最初からコンパイルするためです。

考えられる不利な点は次のとおりです(実装と、それが実際の不利な点であるかどうかによって、ここでも異なります)。

  • ナイーブバリアントよりも多くの割り当てによるメモリ使用量の増加
  • メンテナンスの労力の増加(少なくとも転送機能を作成する必要があります)
  • パフォーマンスの低下(クラスのナイーブな実装の場合のように、コンパイラーはインライン化できない場合があります)

したがって、すべてに慎重に価値を与え、それを自分で評価してください。私にとって、ほとんどの場合、PIMPLイディオムを使用することは努力する価値がないことがわかります。私が個人的にそれを使用する(または少なくとも類似したもの)ケースは1つだけです:

statLinux呼び出し用の私のC++ラッパー。ここで、Cヘッダーの構造体は、設定内容によって異なる場合があります#defines。また、ラッパーヘッダーですべてを制御することはできないため#include <sys/stat.h>.cxxファイル内でのみこれらの問題を回避します。

于 2012-01-23T14:04:33.840 に答える
33

私は商品について他のすべてに同意しますが、制限についての証拠を入れさせてください:テンプレートではうまく機能しません

その理由は、テンプレートのインスタンス化には、インスタンス化が行われた場所で利用可能な完全な宣言が必要なためです。(これが、.cppファイルに定義されたテンプレートメソッドが表示されない主な理由です。)

テンプレート化されたサブクラスを参照することはできますが、それらをすべて含める必要があるため、コンパイル時の「実装デカップリング」のすべての利点(プラットフォーム固有のすべてのコードをどこにでも含めることを避け、コンパイルを短縮する)が失われます。

これは、従来のOOP (継承ベース)には適したパラダイムですが、ジェネリックプログラミング(特殊化ベース)には適していません。

于 2012-01-23T15:21:02.457 に答える
25

他の人々はすでに技術的な長所/短所を提供していますが、私は以下が注目に値すると思います:

何よりもまず、独断的にならないでください。PIMPLが状況に応じて機能する場合は、それを使用してください。「実装が実際に隠されているため、OOの方が優れている」などの理由で使用しないでください。C++ FAQの引用:

カプセル化はコード用であり、人用ではありません(ソース

オープンソースソフトウェアの例とその理由を説明します。OpenThreads、OpenSceneGraphで使用されるスレッドライブラリ。内部状態変数(スレッドハンドルなど)はプラットフォームごとに異なるため、主なアイデアは、ヘッダー(たとえば<Thread.h>)からすべてのプラットフォーム固有のコードを削除することです。このように、すべてが隠されているため、他のプラットフォームの特異性を知らなくても、ライブラリに対してコードをコンパイルできます。

于 2012-01-23T14:08:33.867 に答える
12

私は主に、他のモジュールによってAPIとして使用されることが公開されているクラスのPIMPLを検討します。これには、PIMPL実装で行われた変更の再コンパイルがプロジェクトの残りの部分に影響を与えないため、多くの利点があります。また、APIクラスの場合、バイナリ互換性を促進します(モジュール実装の変更は、それらのモジュールのクライアントに影響を与えません。新しい実装には同じバイナリインターフェイス(PIMPLによって公開されるインターフェイス)があるため、再コンパイルする必要はありません)。

すべてのクラスでPIMPLを使用する場合、これらの利点はすべてコストがかかるため、注意が必要です。実装メソッドにアクセスするには、追加のレベルの間接参照が必要です。

于 2012-01-23T14:04:29.160 に答える
6

これはデカップリングの最も基本的なツールの1つだと思います。

組み込みプロジェクト(SetTopBox)でPIMPL(およびExceptional C ++の他の多くのイディオム)を使用していました。

私たちのプロジェクトにおけるこのイディオムの特定の目的は、XImplクラスが使用するタイプを非表示にすることでした。具体的には、これを使用して、さまざまなハードウェアの実装の詳細を非表示にしました。さまざまなヘッダーが取り込まれます。プラットフォームごとにXImplクラスの実装が異なり、他のプラットフォームでは実装が異なります。クラスXのレイアウトは、プラットフォームに関係なく同じままでした。

于 2012-01-23T13:59:48.070 に答える
5

私は過去にこのテクニックをよく使用していましたが、その後、自分自身がそれから離れていくことに気づきました。

もちろん、クラスのユーザーから実装の詳細を隠すことをお勧めします。ただし、クラスのユーザーに抽象インターフェイスを使用させ、実装の詳細を具象クラスにすることで、これを行うこともできます。

pImplの利点は次のとおりです。

  1. このインターフェースの実装が1つしかない場合、抽象クラス/具象実装を使用しない方が明確です。

  2. 複数のクラスが同じ「impl」にアクセスするようなクラスのスイート(モジュール)があるが、モジュールのユーザーは「公開された」クラスのみを使用する場合。

  3. これが悪いことであると想定される場合、vテーブルはありません。

私が見つけたpImplの欠点(抽象インターフェースの方がうまくいく)

  1. 「本番」実装は1つしかない場合がありますが、抽象インターフェイスを使用することで、単体テストで機能する「モック」実装を作成することもできます。

  2. (最大の問題)。unique_ptrと移動の時代以前は、pImplの保存方法に関して選択肢が制限されていました。生のポインタで、クラスがコピーできないという問題がありました。古いauto_ptrは、前方に宣言されたクラスでは機能しません(とにかくすべてのコンパイラで機能するわけではありません)。そのため、人々は、クラスをコピー可能にするのに便利なshared_ptrを使い始めましたが、もちろん、両方のコピーには、予想外の同じ基になるshared_ptrがありました(一方を変更すると、両方が変更されます)。したがって、解決策は、多くの場合、内部ポインターにrawポインターを使用し、クラスをコピー不可にして、代わりにshared_ptrを返すことでした。したがって、newへの2つの呼び出し。(実際には、3つ与えられた古いshared_ptrが2つ目のものを与えました)。

  3. constnessはメンバーポインタに伝播されないため、技術的には実際にはconst-correctではありません。

したがって、一般的に、私は何年にもわたってpImplから、代わりに抽象インターフェイスの使用法(およびインスタンスを作成するためのファクトリメソッド)に移行しました。

于 2015-08-10T16:16:27.687 に答える
3

他の多くの人が言ったように、Pimplイディオムは、パフォーマンスの低下(追加のポインター間接参照)と追加のメモリーの必要性(メンバーポインター自体)のコストを伴いながら、完全な情報隠蔽とコンパイルの独立性に到達することを可能にします。組み込みソフトウェアの開発、特にメモリを可能な限り節約する必要があるシナリオでは、追加コストが重要になる可能性があります。インターフェイスとしてC++抽象クラスを使用すると、同じコストで同じメリットが得られます。これは実際にはC++の大きな欠陥を示しており、Cのようなインターフェイス(パラメーターとして不透明なポインターを持つグローバルメソッド)を繰り返さないと、追加のリソースの欠点なしに真の情報隠蔽とコンパイルの独立性を得ることができません。これは主にクラスの宣言。これは、そのユーザーが含める必要があります。

于 2015-09-04T11:13:58.230 に答える
3

これが私が遭遇した実際のシナリオで、このイディオムが大いに役立ちました。最近、ゲームエンジンでDirectX11と既存のDirectX9のサポートをサポートすることにしました

エンジンはすでにほとんどのDX機能をラップしているため、DXインターフェイスはどれも直接使用されていません。それらは、ヘッダーでプライベートメンバーとして定義されただけです。エンジンはDLLファイルを拡張機能として使用し、他の多くの拡張機能と同様に、キーボード、マウス、ジョイスティック、およびスクリプトのサポートを追加します。これらのDLLのほとんどはDXを直接使用していませんでしたが、DXを公開するヘッダーを取得したという理由だけで、知識とDXへのリンクが必要でした。DX 11を追加する際に、この複雑さは劇的に増加しましたが、不必要でした。DXメンバーをソースでのみ定義されているPIMPLに移動することで、この面付けを排除しました。

このライブラリの依存関係の削減に加えて、プライベートメンバー関数をPIMPLに移動し、前面のインターフェイスのみを公開することで、公開されたインターフェイスがよりクリーンになりました。

于 2016-11-03T19:46:59.903 に答える
2

多くのプロジェクトで実際に使用されています。その有用性は、プロジェクトの種類に大きく依存します。これを使用する最も著名なプロジェクトの1つはQtであり、基本的な考え方は、実装またはプラットフォーム固有のコードをユーザー(Qtを使用する他の開発者)から隠すことです。

これは高潔なアイデアですが、これには本当の欠点があります。デバッグプライベート実装に隠されたコードが高品質である限り、これはすべて問題ありませんが、そこにバグがある場合、ユーザー/開発者は問題を抱えていますなぜなら、たとえ彼/彼女が実装のソースコードを持っていたとしても、それは隠された実装への単なるばかげたポインタだからです。

したがって、ほぼすべての設計上の決定と同様に、賛否両論があります。

于 2012-01-23T14:08:16.153 に答える
1

私が見ることができる利点の1つは、プログラマーが特定の操作をかなり高速に実装できることです。

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS:移動のセマンティクスを誤解していないことを願っています。

于 2016-09-21T01:45:42.830 に答える
0

何人かの著者がこれをほのめかしましたが、私はその点が十分に明確にされていないと思ったので、私は答えを追加しようと思いました。

PIMPLの主な目的は、N*M問題を解決することです。この問題は他の文献で他の名前を持っているかもしれませんが、簡単な要約はこれです。

ある種の継承階層があり、階層に新しいサブクラスを追加する場合は、N個またはM個の新しいメソッドを実装する必要があります。

私は最近これに気付いたばかりであり、私自身の承認により、まだこれについての専門家ではないので、これはおおよその手に負えない説明にすぎません。

なされた既存のポイントの議論

しかし、私はこの質問と数年前に同様の質問に出くわしました、そして私は与えられた典型的な答えに混乱しました。(おそらく、私は数年前にPIMPLについて最初に学び、この質問とそれに類似した他の質問を見つけました。)

  1. バイナリ互換性を有効にします(ライブラリを作成する場合)
  2. コンパイル時間を短縮します
  3. データを非表示にします

上記の「利点」を考慮すると、私の意見では、それらのどれもPIMPLを使用する特に説得力のある理由ではありません。したがって、私はそれを使用したことがなく、PIMPLの有用性とそれを実際に使用して達成できることを破棄したため、結果としてプログラムの設計に支障をきたしました。

説明するためにそれぞれにコメントさせてください:

1.1。

バイナリの互換性は、ライブラリを作成する場合にのみ関係します。最終的な実行可能プログラムをコンパイルしている場合、他の誰か(バイナリ)ライブラリを使用していない限り、これは関係ありません。(つまり、元のソースコードがありません。)

これは、この利点の範囲と有用性が限られていることを意味します。これは、プロプライエタリ形式で出荷されるライブラリを作成する人だけが関心を持っています。

2.2。

コンパイル時間が非常に重要なプロジェクトで作業することがめったにない現代では、これが関連性があると個人的には考えていません。多分これはGoogleChromeの開発者にとって重要です。おそらく開発時間を大幅に増加させる関連する不利な点は、おそらくこの利点を相殺する以上のものです。私はこれについて間違っているかもしれませんが、特に最近のコンパイラとコンピュータの速度を考えると、それはありそうもないと思います。

3.3。

PIMPLがここにもたらす利点はすぐにはわかりません。ヘッダーファイルとバイナリオブジェクトファイルを出荷しても、同じ結果が得られます。私の目の前に具体的な例がなければ、PIMPLがここで関連している理由を理解することは困難です。関連する「もの」は、元のソースコードではなく、バイナリオブジェクトファイルを出荷することです。

PIMPLが実際に行うこと:

あなたは私の少し手を振る答えを許さなければならないでしょう。私はソフトウェア設計のこの特定の分野の完全な専門家ではありませんが、少なくともそれについて何かをお話しすることができます。この情報は、ほとんどの場合、デザインパターンから繰り返されます。著者はそれを「ブリッジパターン」、別名ハンドル、別名ボディと呼んでいます。

この本では、ウィンドウマネージャを作成する例を示します。ここで重要な点は、ウィンドウマネージャーがさまざまな種類のウィンドウとさまざまな種類のプラットフォームを実装できることです。

たとえば、

  • アイコンウィンドウ
  • 3Dアクセラレーションを備えたフルスクリーンウィンドウ
  • 他のいくつかの派手なウィンドウ
  • これらはレンダリングできるウィンドウの種類です

と同様

  • MicrosoftWindowsの実装
  • OSXプラットフォームの実装
  • LinuxXウィンドウマネージャー
  • Linux Wayland
  • これらはさまざまなタイプのレンダリングエンジンであり、OS呼び出しが異なり、機能も根本的に異なる可能性があります。

上記のリストは、別のユーザーがDVDプレーヤーなどのさまざまな種類のハードウェアで動作するソフトウェアの作成について説明した別の回答に記載されているリストと類似しています。(例が何であったかを正確に忘れています。)

ここでは、デザインパターンの本に書かれているものとは少し異なる例を示します。

重要なのは、継承階層を使用して実装する必要があるのは2つの異なるタイプですが、ここでは単一の継承階層を使用するだけでは不十分です。(N * Mの問題、複雑さは各箇条書きリストの数の2乗のようにスケーリングしますが、これは開発者が実装するのは現実的ではありません。)

したがって、PIMPLを使用すると、ウィンドウのタイプを分離し、実装クラスのインスタンスへのポインターを提供します。

だからPIMPL:

  • N*M問題を解決します
  • 継承を使用してモデル化されている2つの根本的に異なるものを切り離し、1つのモノリスではなく、2つ以上の階層が存在するようにします。
  • 正確な実装動作の実行時交換を許可します(ポインターを変更することにより)。これは状況によっては有利な場合がありますが、単一のモノリスは実行時の動作の選択ではなく静的(コンパイル時)の動作の選択を強制します

これを実装する方法は他にもあるかもしれませんが、たとえば多重継承を使用する場合などですが、少なくとも私の経験では、これは通常、より複雑で難しいアプローチです。

于 2021-12-28T18:14:19.733 に答える