オリジナルへのリンク
これは、 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(再利用およびトラックバック付き)を推奨できます。
しかし、起こりうる困難を恐れないプロジェクトは、実装の完全な変更を可能にするインターフェースの顕著な安定性によって報われるでしょう。
ソース
- ジョン・ラコス; 大規模なC ++ソフトウェア設計。 アディソン・ウェスリー・ロングマン、1996
- ハーブサッター; 優れたC ++:47のエンジニアリングパズル、プログラミングの問題、およびソリューション。 アディソン・ウェスリー・ロングマン、2000
- ハーブサッター、アンドレイアレクサンドレスク:C ++コーディング標準:101ルール、ガイドライン、ベストプラクティス。 アディソン・ウェスリー・ロングマン、2004
- マーク・ムッツ; Pimp my Pimpl; C ++:Vor- und Nachteile des d-Zeiger-Idioms、Teil 1; Artikel auf heise Developer( 英語翻訳あり )
翻訳者メモ
1以下:Pimplは、テレビ番組「Pimp my Ride」(英語の「Pimp my Ride」)への参照である、pimpへの動詞と調和しています。