記事「Pimp My Pimpl」の翻訳、パート1

記事の最初の部分では、古典的なPimplイディオム(実装へのポインター、実装へのポインター)を検討し、その利点を示し、それに基づくイディオムのさらなる開発を検討します。 第2部では、Pimplを使用するときに必然的に発生する欠陥を減らす方法に焦点を当てます。



オリジナルへのリンク



これは、 Heise Developerの記事の最初の部分の翻訳です。 両方のパーツのオリジナルはこちらです: パート1パート2

翻訳はここからの英語の翻訳から作られました。



注釈



馬鹿げた名前Pimplのイディオムについて多くのことが書かれています。 Heise Developerは、従来のテクノロジーを超えたこの実践的な設計のいくつかの側面を強調しています。



クラシックイディオム



すべてのC ++プログラマーは、次のようなクラス記述に出くわす必要があります。



class Class { // ... private: class Private; //   Private *d; //    };
      
      





ここで、Class Class



データフィールドは、ネストされたクラスClass::Private



転送されます。 Class



クラスのインスタンスには、 Class



Class::Private



オブジェクトへのd



ポインターのみが含まれます。



クラスの作成者がこのような非表示を使用した理由を理解するには、戻ってC ++モジュールシステムを確認する必要があります。 他の多くの言語とは異なり、Cの後継であるC ++には組み込みのモジュールサポートがありません(このサポートはC ++ 0xに対して提供されましたが、最終標準には含まれていませんでした)。 代わりに、モジュール関数の宣言(通常はその説明ではない)がヘッダーファイルに配置され、 #include



プリプロセッサディレクティブを使用して他のモジュールで使用できるようにするアプローチが使用されます。 このアプローチは、ヘッダーファイルに二重の役割を与えます。一方で、それらはモジュールのインターフェイスです。 一方、内部実装の可能な詳細の発表の場所によって。



Cでは、このアプローチはうまく機能しました。関数の実装の詳細は、宣言と説明を分離することで完全にカプセル化されました。 構造体の事前宣言を行うことができます(この場合構造体はプライベートになります)か、ヘッダーファイル構造体を直接記述します(その後、構造体はパブリックになります)。 「オブジェクト指向C」では、上記のClass



は次のようになります。



 struct Class; //   typedef struct Class * Class_t; // ->    void Class_new(Class_t *cls); // Class::Class() void Class_release(Class_t cls); // Class::~Class() int Class_f(Class_t cls, double num); // int Class::f(double) // ...
      
      





残念ながら、これはC ++では機能しません。 メソッドはクラス内で宣言する必要があります。 メソッドのないクラスは役に立たないため、通常、C ++ヘッダーファイルにはクラスの説明が含まれています。 名前空間とは異なり、クラス本体を再度開くことはできないため、ヘッダーファイルにはすべての宣言(データフィールドとメソッド)を含める必要があります。



 class Class { public: // ...   ... ok private: // ...     ...   ,     };
      
      





問題は明らかです。モジュールインターフェイス(ヘッダーファイル)には必ず実装の詳細が含まれます-悪いアプローチです。 したがって、すべての実装の詳細(データフィールドとプライベートメソッド)が別のクラスに配置される場合、かなり粗雑なトリックが使用されます。



 // --- class.h --- class Class { public: Class(); ~Class(); // ...   ... void f(double n); private: class Private; Private *d; }; // -- class.cpp -- #include "class.h" class Class::Private { public: // ...     ... bool canAcceptN(double num) const { return num != 0 ; } double n; }; Class::Class() : d(new Private) {} Class::~Class() { delete d; } void Class::f(double n) { if (d->canAcceptN(n)) d->n = n; }
      
      





Class::Private



、ポインター変数を宣言する場合にのみ使用されるため、 「サイズで」ではなく「名前でのみ」(Lakos)、純粋なCの場合のように、事前宣言で十分です。 Class



クラスのすべてのメソッドは、プライベートフィールドとClass::Private



データフィールドにアクセスします。 。



したがって、C ++で完全にカプセル化されたモジュールシステムの利便性が得られます。 中間変数の使用により、メモリのオーバーヘッド割り当て( new Class::Private



)、データフィールドへの間接アクセス、 inline



メソッドの完全な拒否(少なくともパブリックセクション)によって得られる利点を支払う必要があります。 記事の第2部で示すように、定数メソッドのセマンティクスも変わります。



上記の欠点の修正または少なくとも軽減に専念するこの記事の第2部の前に、検討中のイディオムの適用がもたらす利点を説明しようとします。



Pimplイディオムの利点



Pimplを使用するメリットはかなりあります。 すべての実装の詳細をカプセル化することにより、薄くて長期的な安定したインターフェイス(ヘッダーファイル)が得られます。 最初は、クラスの読みやすい説明です。 2番目の下-実装の大幅な変更後でもバイナリ互換性のサポート。



たとえば、Qt 4クラスライブラリの開発中にノキアのQt開発フレームワーク(以前のTrolltech)は、Qt 4を使用してアプリケーションを再リンクする必要なく、ウィジェットのレンダリングに大幅な変更を加えました。



特に大規模なプロジェクトでPimplのイディオムを使用する場合、アセンブリの大幅な加速を過小評価しないでください。 ヘッダーファイル内の#include



ディレクティブの数が減少し、Pimplクラスのヘッダーファイルを変更する頻度が大幅に減少したため、アセンブリが高速化されました。 「C ++での複雑な問題の解決」(「Exceptional C ++」)という本の中で、Herb Sutterはコンパイル速度が絶えず倍増していることを指摘し、John Lakosはアセンブリが2桁加速されると主張しています。



Pimplを使用する別の利点:dポインターを持つクラスは、トランザクション指向で例外に対して安全なコードに適しています。 たとえば、開発者はコピースワップイディオム(Satter、Alexandrescu“ C ++ Programming Standards”、パラグラフ56)を使用して、トランザクション(全か無か)のコピー割り当て演算子を作成できます。



 class Class { // ... void swap(Class &other) { std::swap(d, other.d); } Class &operator=(const Class &other) { //    ,    *this Class copy(other); //     ,    *this swap(copy); return *this; }
      
      





C ++ 0xでの移動操作の実装簡単です(特に、すべてのPimplクラスで同じです)。



  //    C++0x: Class(Class &&other) : d(other.d) { other.d = 0; } Class &operator=(Class &&other) { std::swap(d, other.d); return *this; } // ... };
      
      





このモデルでは、クラスのカプセル化を損なうことなく、交換関数と代入演算子をinline



として実装できます。 開発者はこの機能を効果的に使用できます。



高度な作曲方法



注目に値するPimplの最後の利点は、データフィールドの直接集約を使用して追加の動的メモリ割り当てを削減できることです。 Pimplを使用しない場合、集約は通常、ポインターを使用してクラスを相互に分離します(データフィールドにPimplを使用)。 クラス全体にPimplを使用すると、ポインターのみで複雑なタイプのプライベートデータを保存する必要がなくなります。



たとえば、慣用的なQtダイアログクラス



 class QLineEdit; class QLabel; class MyDialog : public QDialog { // ... private: //   Qt: QLabel *m_loginLB; QLineEdit *m_loginLE; QLabel *m_passwdLB; QLineEdit *m_passwdLE; };
      
      





になります



 #include <QLabel> #include <QLineEdit> class MyDialog::Private { // ... //    Qt,       QLabel loginLB; QLineEdit loginLE; QLabel passwdLB; QLineEdit passwdLE; };
      
      





Qtの愛好家は、 QDialog



デストラクタがすでに子孫のウィジェットを破棄していることに気付く可能性があります。したがって、直接集約によってその子孫が二重に破壊されます。 実際、この手法を使用すると、特にデータフィールドもクラスに属し、その逆の場合、メモリ割り当てシーケンスでエラーが発生するリスクがあります(二重削除、割り当て解除後の使用など)。 ただし、この場合、示されている変換は安全です。なぜなら、 Qtでは、常に親の前の子を削除できます。



この方法で集約されたデータフィールド自体がPimplクラスのインスタンスである場合、このアプローチは特に効果的です。 これは、Pimplイディオムを使用するとサイズがsizeof(void*)



4つの動的メモリ割り当てを保持する最後の例の場合とまったく同じです。代わりに、追加の(大)メモリ割り当ては1つだけです。 これにより、ヒープをより効率的に使用できます。 小さいメモリ割り当ては、アロケータに常に大きなオーバーヘッドを作成します。



さらに、このアプローチでは、コンパイラは仮想関数呼び出しを「開発」する可能性が非常に高くなります。 呼び出された関数の仮想性が導く二重間接呼び出しを削除します。 ポインター集約を使用する場合、これにはプロシージャー間の最適化が必要です。 いずれの場合でも、これにより、追加の間接呼び出しの背景に対して実行時のパフォーマンスが向上します。 ただし、特定のクラスをプロファイリングして、必要に応じてdポインターをチェックする必要があります。



プロファイリングによって動的メモリ割り当てがボトルネックになっていることが示されている場合、「Fast Pimpl」というイディオム(「C ++での複雑な問題の解決」、段落30)の使用が役立ちます。 この実施形態では、グローバル演算子new()



代わりにPrivate



クラスのインスタンスを作成するために、例えばboost::singleton_pool



などの高速アロケータが使用されます。



中間結論



Pimplはよく知られたC ++イディオムであり、プログラマがクラスのインターフェイスを実装から分離し、C ++が直接実行できない程度まで可能にします。 d-pointerを使用することのプラスの副作用は、コンパイルの高速化、トランザクションセマンティクスの簡単な実装、および高度な構成メソッドを使用して実行時に潜在的に効率的な実装を行う機能です。



しかし、dポインターには欠点もあります。追加のPrivate



クラスを作成する必要があることに加えて、そのための動的メモリ割り当て、定数メソッドのセマンティクスの変更、メモリ割り当てシーケンスの潜在的なエラーも懸念の原因です。



記事の第2部では、著者はこれらの問題のいくつかに対する解決策を示します。

複雑さはさらに増加するため、それぞれの場合に、イディオムを使用する利点がその欠点を上回るかどうかを確認する必要があります。 疑わしい場合は、疑わしいクラスごとにこのようなチェックを行う必要があります。 いつものように、一般的な解決策はありません。



次は?



この記事の2番目(および最後)の部分では、内部Pimplデバイスを紹介し、問題のある領域を明らかにし、イディオムをいくつかの改善で補完します。



オタク誌のPimplに関するその他の記事



イディオムPimplおよびFast Pimpl-実装へのポインター

QtのPimplとは何ですか?

Qt Pimplパターンのプライベートスロット



All Articles