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 *型であり、ポインターdClass::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 *ddeep_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::PrivateBase::_d代わりにDerided::Privateに渡しDerided::Private (異なるコンテキストで同じPrivate名を使用していることに注意してください)。 また、著者は、強制的な型変換を行うBaseメソッドに関して、 d_func()メソッドを実装しています。

Baseコンストラクターが正しく機能するためには、 Base::PrivateDerived::Private祖先でなければなりません:
 class Derived::Private : public Base::Private { // ... }; 

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

最初に、開発者はBase::Privateデストラクタを仮想にする必要があります。 そうしないと、 Baseデストラクタがトリガーされたときに未定義の動作が発生し、 Base::Privateへのポインタを介してDerived::Private実装オブジェクトを削除しようとします。

次に、 Privateは通常エクスポートテーブルに該当しないため、開発者は同じライブラリに両方のクラスを実装する必要がありますdeclspec(dllexport)で指定されておらず、ELFバイナリでvisibility=hiddenとしてリストされていません。 ただし、 BaseDerived異なるライブラリに実装されて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への動詞と調和しています。

Source: https://habr.com/ru/post/J137702/


All Articles