Pimp my Pimplの翻訳、パート2

尊敬されるskb7Pimplのイディオム(実装へのポインター)によって翻訳された記事の最初の部分では、その目的と利点を調べました。 第二部では、このイディオムを使用するときに生じる問題を検討し、それらを解決するためのいくつかのオプションを提案します。



オリジナルへのリンク



これは、 Heise Developer Webサイトで公開されている記事の2番目の部分の翻訳です。 最初の部分の翻訳はここで見つけることができます 。 両方の部分のオリジナル(ドイツ語)はこちらこちらです。



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



注釈



d-pointer、コンパイラファイアウォール、またはCheshire Catとも呼ばれる、このおかしな音のイディオムについて多くのことが書かれています。 Pimplイディオムの古典的な実装とその利点を紹介したHeise Developerの最初の記事の後に、Pimmplイディオムを使用するときに必然的に生じる問題のいくつかを解決するこの2番目と最後の記事が続きます。



パート2



定数違反


最初のニュアンスは明らかではありませんが、オブジェクトのフィールドの不変性の解釈に関連しています。 Pimplイディオムを使用する場合、メソッドはd



ポインターを介して実装オブジェクトのフィールドにアクセスします。

 SomeThing & Class::someThing() const { return d->someThing; }
      
      





この例を注意深く調べてみると、このコードはC ++で定数オブジェクトを保護するためのメカニズムをバイパスしていることがsomeThing()



ます。メソッドはconst



として宣言されているため、 someThing()



メソッド内のsomeThing()



ポインターはconst Class *



型であり、ポインターd



Class::Private * const



型ですClass::Private * const



ただし、これは、 Class::Private



クラスからのフィールドへのアクセスの変更を防ぐのに十分ではありません*d



は定数ですが、 *d



はそうではないためです。



要確認:C ++では、 const



修飾子の位置が重要です。

 const int * pci; //    int int * const cpi; //    int const int * const cpci; //     int *pci = 1; // : *pci  *cpi = 1; // : *cpi   *cpci = 1; // : *cpci  int i; pci = &i; //  cpi = &i; // : cpi  cpci = &i; // : cpci 
      
      





したがって、Pimplのイディオムを使用すると、すべてのメソッド(およびconst



として宣言されたメソッド)が実装オブジェクトのフィールドを変更できます。 Pimplを使用していなかった場合、コンパイラはそのようなエラーをキャッチできたはずです。



この型システムの欠陥は通常望ましくないため、対処する必要があります。 これには、ラッパークラスdeep_const_ptr



またはいくつかのd_func()



メソッドの2つのメソッドを使用できます。 最初の方法は、選択したポインターに一定性を課すスマートポインターを実装することです。 このクラスの定義は次のとおりです。

 template <typename T> class deep_const_ptr { T * p; public: explicit deep_const_ptr( T * t ) : p( t ) {} const T & operator*() const { return *p; } T & operator*() { return *p; } const T * operator->() const { return p; } T * operator->() { return p; } };
      
      





operator*()



およびoperator->()



メソッドの定数および通常バージョンをオーバーロードするトリックを使用して、 *d



オブジェクトにd



ポインター定数を課すことができます。 Private *d



deep_const_ptr<Private> d



置き換えると、問題が完全に解決します。 しかし、そのような解決策は冗長かもしれません:この状況では、参照解除演算子をオーバーロードするトリックをClass



クラスに直接適用できます:

 class Class { // ... private: const Private * d_func() const { return _d; } Private * d_func() { return _d; } private: Private * _d; };
      
      





ここで、メソッド実装で_d



を使用する代わりに、 d_func()



を呼び出す必要があります。

 void Class::f() const { const Private * d = f_func(); //  'd' ... }
      
      





もちろん、メソッド内の_d



への直接アクセスを妨げるものは何もありません。これは、スマートポインターdeep_const_ptr



を使用する場合には発生しません。 したがって、 Class



クラスのメソッドをオーバーロードするメソッドには、開発者からの規律が必要です。 さらに、 deep_const_ptr



クラスの実装を変更して、 Class



オブジェクトの破棄時に作成されたPrivate



オブジェクトを自動的に削除できます。 また、クラスメソッドのオーバーロードは、多相クラスの階層の作成に役立ちます。これについては後で説明します。



コンテナクラスへのアクセス


開発者がClass



クラスのすべてのprivate



メソッドをPrivate



クラスに入れると、次の障害が発生します。現在、これらのメソッドでは、 Class



クラスの他の(非static



)メソッドを呼び出すことができません。

 class Class::Private { public: Private() : ... {} // ... void callPublicFunc() { /*???*/Class::publicFunc(); } }; Class::Class() : d( new Private ) {}
      
      





この問題は、バックリンクを導入することで解決できます( q



フィールドの名前はQtコードに記載されています)。

 class Class::Private { Class * const q; //   public: explicit Private( Class * qq ) : q( qq ), ... {} // ... void callPublicFunc() { q->publicFunc(); } }; Class::Class() : d( new Private( this ) ) {}
      
      





後方参照を使用する場合、 Private



コンストラクターが機能するまでd



初期化は完了しなかったことを覚えておくことが重要です。 開発者は、 Private



コンストラクターの本体のd



フィールドにアクセスするClass



メソッドを呼び出さないでください。呼び出さないと、未定義の動作が発生します。



再保険の場合、開発者は、nullポインターでバックリンクを初期化し、 Class



コンストラクターの本体でPrivate



コンストラクターを実行した後にのみ正しいリンク値を設定する必要があります。

 class Class::Private { Class * const q; // back-link public: explicit Private( /*Class * qq*/ ) : q( 0 ), ... {} // ... }; Class::Class() : d( new Private/*( this )*/ ) { //   : d->q = this; }
      
      





上記の制限にもかかわらず、通常、クラス初期化コードの大部分をPrivate



コンストラクターに転送できます。これは、いくつかのコンストラクターを持つクラスにとって重要です。 また、 q



ポインター(後方リンク)を使用すると、既に検討されている恒常性違反の問題が発生し、同様の方法で解決できることにも言及する価値があります。



小計


Pimplイディオムによるプライベート実装クラスの導入で失われた機能を復元できたので、記事の残りの部分では、Pimplイディオムの使用時に発生する追加のメモリコストを平準化できる「魔法」に専念します。



再利用可能なオブジェクトでパフォーマンスを改善する


優れたC ++開発者である読者は、古典的なPimplイディオムを説明する記事への注釈を読んだ後、おそらく懐疑論に満ちているでしょう。 特に、追加のメモリ割り当ては、特にそれ自体はほとんどメモリを必要としないクラスに関して、非常に不利になる可能性があります。



まず、コードをプロファイリングしてそのような考慮事項を確認する必要がありますが、これが潜在的なパフォーマンスの問題の解決策の検索を拒否する理由にはなりません。 記事の最初の部分では、実装オブジェクトへのクラスフィールドの埋め込みについて既に説明しました。これにより、メモリ割り当て要求の数が削減されました。 次に、さらに高度な別の手法、実装ポインタの再利用について検討します。



ポリモーフィッククラスの階層では、追加のメモリコストの問題は階層の深さによって悪化します。各階層クラスには、新しいフィールドを持たない場合でも、独自の隠された実装があります(たとえば、クラスの新しいメンバーを導入せずに仮想メソッドを再定義するための継承)。



開発者は、継承クラスで基本クラスのd



ポインターを再利用することで、 d



ポインター(および関連するメモリ割り当て)の数の増加に対処できます。

 // base.h: class Base { // ... public: Base(); protected: class Private; explicit Base( Private * d ); Private * d_func() { return _d; } const Private * d_func() const { return _d; } private: Private * _d; }; // base.cpp: Base::Base() : _d( new Private ) { // ... } Base::Base( Private * d ) : _d( d ) { // ... }
      
      





パブリックコンストラクターに加えて、 protected



コンストラクターが存在することにより、継承クラスが基本クラスにd



ポインターを埋め込むことができます。 また、このコードは、継承クラスの_d



への(変更しない)アクセスのために、 d_func()



メソッド(現在はprotected



いますd_func()



を使用するconst



correctness修正も使用し_d





 // derived.h: class Derived : public Base { public: Derived(); // ... protected: class Private; Private * d_func(); //    const Private * d_func() const; //  }; // derived.cpp: Derived::Private * Derived::d_func() { return static_cast<Private*>( Base::d_func() ); } const Derived::Private * Derived::d_func() const { return static_cast<const Private*>( Base::d_func() ); } Derived::Derived() : Base( new Private ) {}
      
      





現在、 Derived



者は新しいBase



コンストラクタを使用して、 Base::Private



Base::_d



代わりにDerided::Private



に渡しDerided::Private



(異なるコンテキストで同じPrivate



名を使用していることに注意してください)。 また、著者は、強制的な型変換を行うBase



メソッドに関して、 d_func()



メソッドを実装しています。



Base



コンストラクターが正しく機能するためには、 Base::Private



Derived::Private



祖先でなければなりません:

 class Derived::Private : public Base::Private { // ... };
      
      





実際にBase::Private



からクラスを継承するには、3つの条件を満たす必要があります。



最初に、開発者はBase::Private



デストラクタを仮想にする必要があります。 そうしないと、 Base



デストラクタがトリガーされたときに未定義の動作が発生し、 Base::Private



へのポインタを介してDerived::Private



実装オブジェクトを削除しようとします。



次に、 Private



は通常エクスポートテーブルに該当しないため、開発者は同じライブラリに両方のクラスを実装する必要がありますdeclspec(dllexport)



で指定されておらず、ELFバイナリでvisibility=hidden



としてリストされていません。 ただし、 Base



Derived



異なるライブラリに実装されてDerived



場合、エクスポートは避けられません。 例外的なケースでは、ライブラリのメインクラスのPrivate



クラスがエクスポートされます。たとえば、ノキアの開発者はQObjectPrivate



(QtCoreから)およびQWidgetPrivate



(QtGuiから)クラスをQObject



しました。 ただし、そうすることで、開発者はインターフェイスレベルだけでなく「内部」レベルでもライブラリ間の依存関係を作成するため、異なるバージョンのライブラリの互換性に違反します。一般に、 libQtGui.so.4.5.0



は動的リンカはlibQtCore.so.4.6.0



をそれに接続します。



最後に、3番目に、 Derived::Private



定義が必要とするため、 Base::Private



定義はBase::Private



クラス実装ファイル( base.cpp



)に隠されたままになりません。 Base::Private



定義をどこに置くか? base.h



単純に含めることができますが、内部実装がまだ外部から見える場合のPimplの使用は何ですか? これらの質問に対する答えは、特別なプライベートヘッダーファイルを作成することです。 この目的のために、QtとKDEは_p.h



命名スキームを確立しています(接尾辞_priv



_i



および_impl



_impl



)。 Base::Private



定義に加えて、このプライベートファイルは、コンストラクタなどのBase



メソッドのinline



実装をホストできます。

 inline Base::Base( Private * d ) : _d( d ) {}
      
      





そしてderived_p.h





 inline Derived::Derived( Private * d ) : Base( d ) {} inline const Derived::Private * Derived::d_func() const { return static_cast<const Private*>( Base::d_func() ); } inline Derived::Private * Derived::d_func() { return static_cast<Private*>( Base::d_func() ); }
      
      





厳密に言えば、上記のコードはOne Definition Ruleルールと矛盾しますd_func()



実装は、 derived_p.h



を含むファイルではインラインであり、他のファイルではインラインではないためです。



実際には、これは問題ではありませんd_func()



d_func()



すべての人が、何らかの方法でファイルderived_p.h



を含める必要があるからです。 再保険のために、 derived.h



ファイルのDerived



定義で問題のあるメソッドをinline



として宣言できます-最新のコンパイラーは、実装なしのメソッドでinline



を許可します。



多くの場合、開発者はこの手法で発生する冗長コードをマクロの下に隠します。 たとえば、Qtは、クラス定義で使用するマクロQ_D



と、メソッド実装でポインターd



を宣言し、 d_func()



初期化するマクロQ_D



定義します。



それにもかかわらず、1つの欠点が残っています。開発者が実装ポインタとバックリンクメカニズムの再利用を組み合わせたい場合、いくつかの困難が生じます。 特に、継承階層内のコンストラクターが機能するまで、 Private



コンストラクターに渡されるDerived



へのポインターを間接的に参照しないように注意する必要があります。

 Derived::Private( Derived * qq ) : Base( qq ) // ,   { q->setFoo( ... ); // ,     }
      
      





Derived



、参照解除の瞬間Derived



作成されただけでなく、前述の非ポリフォーマルの場合とは異なります-Base、そのPrivate



フィールドはまだ作成中です。



この場合、以前と同様に、 null



ポインターで後方リンクを初期化する必要がありnull



。 後方リンクを正しい値に設定するタスクは、階層チェーンの最後にあるクラス、つまり、階層内でPrivate



クラスを実装するクラスにかかっています。 Derived



の場合、コードは次のようになります。

 Derived::Derived() : Base( new Private/*( this )*/ ) { d_func()->_q = this; }
      
      





必要に応じて、開発者は逆方向リンクを介したアクセスを必要とする初期化コードを別のメソッドPrivate::init()



(2段階のPrivate



構造を意味するPrivate::init()



転送できます。 このメソッドは、クラスのコンストラクターで(のみ)呼び出され、それ自体がDerived



インスタンスを作成します。

 Derived::Derived( Private * d ) : Base( d ) { // __  d->init()! } Derived::Derived() : Base( new Private ) { d_func()->init( this ); } Derived::Private::init( Derived * qq ) { Base::Private::init( qq ); //  _q //    }
      
      





さらに、各Private



クラスは、コンテナクラスへの独自の後方参照を持っているか、 Base::Private



基本クラスの後方参照の型変換を担当するq_func()



メソッドを定義する必要があります。 対応するコードはここでは示しません-それを書くことは、尊敬される読者のための演習として残ります。 この演習の解決策は、 Heise FTPサーバーで「ポンプされた」 1 Shape



階層として見つけることができます。



結論



よく知られたC ++イディオムであるPimplを使用すると、開発者はC ++組み込みツールでは達成できない範囲でインターフェースと実装を分離できます。 プラスの副作用として、開発者はコンパイルの高速化、トランザクションセマンティクスの実装機能、および構成の積極的な使用を通じて、将来のコードの一般的な高速化を実現します。



d



ポインターを使用する場合、すべてがそれほどスムーズではありません。追加のPrivate



クラス、関連するメモリー割り当て、 const



性の違反、および初期化順序の潜在的なエラーは、開発者にとって大量の血液を台無しにします。 この記事にリストされているすべての問題に対して、多くのコードを書く必要があるソリューションが提案されました。 複雑さが増しているため、少数のクラスまたはプロジェクトに対してのみ、完全に「アップグレードされた」Pimpl(再利用およびトラックバック付き)を推奨できます。



しかし、起こりうる困難を恐れないプロジェクトは、実装の完全な変更を可能にするインターフェースの顕著な安定性によって報われるでしょう。



ソース







翻訳者メモ



1以下:Pimplは、テレビ番組「Pimp my Ride」(英語の「Pimp my Ride」)への参照である、pimpへの動詞と調和しています。



All Articles