D-Pointer/ja
dポインタとは
たとえば、このようなQtソースファイルを開くと、Q_DマクロやQ_Qマクロが多数ちりばめられているのを見つけることができます。この記事は、これらのマクロの目的を解説します。
Q_DおよびQ_Qマクロは、dポインタ(あるいはオペークポインタ)と呼ばれるデザインパターンの一部です。これによって、ライブラリの実装の細部がユーザから隠蔽され、実装の変更もバイナリ互換性を壊すことなく行うことが可能となっています。
バイナリ互換性 — それってなに?
Qtのようなライブラリをデザインする上では、Qtライブラリが他のバージョンにアップグレードされたり置き換えられたりした場合にも、Qtに動的にリンクしているアプリケーションが、再コンパイルすることなく動くことが重要です。たとえば、あなたがCuteAppというアプリをQt 4.5でビルドしたとして、Qt ライブラリがバージョン4.5から4.6にアップグレードしたとしても(Windowsではアプリとともに同梱されますが、Linuxではパッケージマネジャが自動的に導入します!)、あなたのCuteAppは依然として実行可能でなければなりません。
バイナリ互換性を壊すのはなにか?
すると、ライブラリの変更がアプリケーションの再コンパイルを必要とするのは、どんなときでしょうか。以下のような簡単な例を見てみましょう。
class Widget { // ... private: Rect m_geometry; }; class Label : public Widget { public: // ... String text() const { return m_text; } private: String m_text; };
ここには、ジオメトリをメンバー変数に持つウィジェットがあります。このウィジェットをコンパイルして、WidgetLib 1.0として出荷しましょう。
WidgetLib 1.1では、いいアイディアを思いついた人が居て、スタイルシートのサポートを追加したとします。なんてことは無い、新しいメソッドと新しいデータメンバを追加するだけです。
class Widget { // ... private: Rect m_geometry; String m_stylesheet; // WidgetLib 1.1で追加 }; class Label : public Widget { public: // ... String text() const { return m_text; } private: String m_text; };
上記の変更を加えたWidgetLib 1.1を出荷すると、WidgetLib 1.0でコンパイルしてうまく走っていたCuteAppが、見事にクラッシュすることになります!
どうしてクラッシュしたのか?
その理由は、新しいデータメンバを追加したことによって、ウィジェットとラベルオブジェクトのサイズを変更してしまったからです。どうしてこれが関係あるのでしょうか?それは、お使いのC++コンパイラがコードを生成する際、オブジェクト内のデータにアクセスするのに、コンパイラがオフセットを使用するからです。
上記のPODオブジェクトがメモリ内でどのように見えるか、ひじょうに簡略化した模式図を見てみましょう。
WidgetLib 1.0でのラベルオブジェクトレイアウト | WidgetLib 1.1でのラベルオブジェクトレイアウト |
---|---|
m_geometry <オフセット0> | m_geometry <オフセット0> |
- - - | m_stylesheet <オフセット1> |
m_text <オフセット1> | - - - |
- - - | m_text <オフセット2> |
WidgetLib 1.0では、ラベルのテキストメンバは(論理的)オフセット1の場所にあります。コンパイラがアプリケーション中に生成したコードでは、Label::text()メソッドは、アプリケーション中のラベルオブジェクトから1だけオフセットした位置にアクセスするものと翻訳されるのです。しかし、WidgetLib 1.1では、ラベルのテキストメンバは(論理的)オフセット2にシフトしてしまっています!アプリケーションは、再コンパイルされていないためtextがオフセット1にあるものと考えていますので、stylesheet変数にアクセスしてしまうのです!
ここで、なぜLabel::text()のオフセット計算コードがCuteAppバイナリで終わってしまって、WidgetLibバイナリで終わらないのかと不思議に思う人がいるはずです。その答えは、Label::text()用のコードがヘッダファイルで定義されているので、コンパイラがそれをインライン化するだけで終わってしまうからです。
それでは、Label::text()がインライン化されていなければ事情は変わるのでしょうか。つまり、Label::text()をソースファイルに移動したとしたら?答えはノーです。C++コンパイラは、オブジェクトのサイズがコンパイル時と実行時で同じであることに依存しています。たとえば、スタックワインディング・アンワインディングがそうです。もしラベルオブジェクトをスタックに作成したとすると、コンパイラはスタックに領域を割り当てるのに、コンパイル時のラベルサイズを用いるコードを生成します。WidgetLib 1.1では、実行時のラベルサイズが違うために、ラベルのコンストラクタは既存のスタックデータを上書きしてしまい、スタックの破損をもたらすのです。
エクスポートされたC++クラスのサイズは決して変更してはならない
要約すると、あなたのライブラリが一旦リリースされたならば、(ユーザーに見える形で)エクスポートされたC++クラスのサイズやレイアウト(データを動かし回ってはいけません)は、けっして変更してはなりません。C++コンパイラは、クラス中のデータのサイズや順序は、アプリケーションがコンパイルされた後は変更されないと仮定してコードを生成します。
すると、どうやって、オブジェクトのサイズを変更せずに、新機能を追加できるでしょうか。
dポインタ
ここで上手い方法は、単一のポインタのみを保管することで、ライブラリのすべての公開クラスのサイズを一定に保つ方法です。このポインタは、全てのデータを含む非公開/内部データ構造体をポイントしています。この内部構造体のサイズは、アプリケーションにどんな副作用も及ぼすことなく、縮んだり大きくなったりすることができます。なぜなら、ポインタはライブラリコード内のみでアクセスされ、アプリケーションの視点からは、オブジェクトのサイズは、つねにポインタのサイズであり、けっして変化しないからです。このポインタがdポインタと呼ばれます。
このパターンの精神は、下記のコードにまとめられています(この記事のコードはすべてデストラクタを持っていませんが、もちろん実際のコードではこれらを追加しなくてはなりません)。
widget.h
/* d_ptrはポインタであり、ヘッダファイル中で参照されることは決してないので (その場合コンパイルエラーとなります)、WidgetPrivateをインクルードする 必要はありませんが、代わりに前方宣言する必要があります。 クラスの定義は、widget.cppに書くこともできますし、widget_p.hなどの 別ファイルに書くこともできます。 */ class WidgetPrivate; class Widget { // ... Rect geometry() const; // ... private: WidgetPrivate *d_ptr; };
widget_p.h:これはウィジェットクラスの非公開ヘッダファイルです。
/* widget_p.h (_p は private を表すことにします) */ struct WidgetPrivate { Rect geometry; String stylesheet; };
widget.cpp
// この#includeのおかげで、WidgetPrivateにアクセスできます。 #include "widget_p.h" Widget::Widget() : d_ptr(new WidgetPrivate) { // 非公開データの生成 } Rect Widget::geometry() const { // d-ptrはライブラリコード内でのみアクセスされます return d_ptr->geometry; }
つぎに、ウィジェットに基づいた子クラスの例です。
label.h
class Label : public Widget { // ... String text(); private: // 各クラスは、それぞれのdポインタを保持します LabelPrivate *d_ptr; };
label.cpp
// WidgetPrivateとは違って、執筆者はLabelPrivateを // ソースファイル自身の中で定義することに決めました struct LabelPrivate { String text; }; Label::Label() : d_ptr(new LabelPrivate) { } String Label::text() { return d_ptr->text; }
上記の構造体を用いれば、CuteAppは、dポインタに直接アクセスすることはありません。dポインタはWidgetLib内でのみアクセスされ、WidgetLibは各リリースで再コンパイルされるため、非公開クラスはCuteAppに影響を与えることなく、自由に変更することができます。
dポインタの他の利点
利点はバイナリ互換性に関するものばかりではありません。dポインタには、以下のように他の利点もあります。
- 実装の詳細を隠す − WidgetLibをヘッダファイルとバイナリファイルだけで出荷することができます。.cppファイルは非公開ソースとすることができます。
- ヘッダファイルは、実装の詳細からクリーンであり、API参照として提供することができます。
- 実装に必要なヘッダファイルはヘッダファイルから実装(ソース)ファイルに移されるので、コンパイルがずっと速くなります。
上記の利点が自明に見えるのは、まったく以て事実です。Qtにおいてdポインタを使用する本当の理由は、バイナリ互換性にあり、Qtがクローズドソースとして始まった事実に因ります。
qポインタ
これまでは、dポインタをCスタイルのデータ構造体としてのみ見てきました。実際には、これに非公開メソッド(ヘルパー関数)が含まれます。たとえば、LabelPrivateは、マウスをクリックしたときにリンクターゲットを探すのに必要なgetLinkTargetFromPoint()というヘルパー関数を持っているかもしれません。多くの場合、これらのヘルパーメソッドは、ラベルやベースクラスのウィジェットからの関数など、公開クラスへのアクセスが必要となります。たとえば、setTextAndUpdateWidget() というヘルパーメソッドは、ウィジェットの再描画をスケジュールする公開メソッドであるWidget::update()を呼び出したいかもしれません。そこで、WidgetPrivateには公開クラスへのポインタであるqポインタを保持させるようにします。上記のコードをqポインタ用に修正すると、以下のようになります。
widget.h
class WidgetPrivate; class Widget { // ... Rect geometry() const; // ... private: WidgetPrivate *d_ptr; };
widget_p.h
struct WidgetPrivate { // q-ptrを初期化するコンストラクタ WidgetPrivate(Widget *q) : q_ptr(q) { } Widget *q_ptr; // q-ptrはAPIクラスをポイントしています Rect geometry; String stylesheet; };
widget.cpp
#include "widget_p.h" // 非公開データを生成 // thisポインタをq-ptrを初期化するために渡します Widget::Widget() : d_ptr(new WidgetPrivate(this)) { } Rect Widget::geometry() const { // d-ptrはライブラリコード中のみでアクセスされます return d_ptr->geometry; }
以下は、ウィジェットに基づいた別のクラスです。
label.h
class Label : public Widget { // ... String text() const; private: LabelPrivate *d_ptr; };
label.cpp
// WidgetPrivateとは違って、執筆者はLabelPrivateを // ソースファイル自身の中で定義することに決めました struct LabelPrivate { LabelPrivate(Label *q) : q_ptr(q) { } Label *q_ptr; String text; }; Label::Label() : d_ptr(new LabelPrivate(this)) { } String Label::text() { return d_ptr->text; }
dポインタを継承して最適化する
上記のコードにおいて、ラベルをたった一つ作るだけで、LabelPrivateとWidgetPrivateへのメモリ割当てが行われてしまいました。この方針をQtで採用したとすると、QListWidgetのようなクラスでは、ひじょうに悪い状況を生み出します。このクラスは、クラス継承の階層で第6階層の深みにあるので、6つのメモリ割当てを行ってしまうのです!
この状況は、非公開クラスの継承階層を作り、dポインタによるインスタンス化された経路をたどらせることで解決します。
dポインタを継承する際には、非公開クラスの宣言は、たとえばsidget_p.hといった独立したファイルで行わなくてはならないことに注意してください。dポインタの継承を行うと、widget.cppファイルの中で宣言を行うことはできません。
widget.h
class Widget { public: Widget(); // ... protected: // サブクラスのみ以下にアクセスできます // サブクラスにそれら自身の具体的なPrivateを初期化するのを許容します Widget(WidgetPrivate &d); WidgetPrivate *d_ptr; };
widget_p.h
struct WidgetPrivate { WidgetPrivate(Widget *q) : q_ptr(q) { } // q-ptrを初期化するコンストラクタ Widget *q_ptr; // APIクラスをポイントするq-ptr Rect geometry; String stylesheet; };
widget.cpp
Widget::Widget() : d_ptr(new WidgetPrivate(this)) { } Widget::Widget(WidgetPrivate &d) : d_ptr(&d) { }
label.h
class Label : public Widget { public: Label(); // ... protected: Label(LabelPrivate &d); // Labelサブクラスが自身のPrivateに渡すのを許容します // Labelにd_ptrがなくなっていることに注意!Widgetのd_ptrを使っているだけです };
label.cpp
#include "widget_p.h" class LabelPrivate : public WidgetPrivate { public: String text; }; Label::Label() : Widget(*new LabelPrivate) // 自身のdポインタを初期化 { } Label::Label(LabelPrivate &d) : Widget(d) { }
この美しさがわかるでしょうか?ここでは、Labelオブジェクトを生成する際に、(WidgetPrivateの下層クラスである)LabelPrivateを作り出します。そして、LabelPrivateはウィジェットの保護されたコンストラクタへの具象化されたdポインタを渡すのです!ここではLabelオブジェクトが生成される際に、1つのメモリ割当てしか起こりません。Label自身も保護されたコンストラクタを持っているので、その下層クラスは自身の非公開クラスを提供するのにこれを用いることができます。
Qtにおけるdポインタ
Qtでは、事実上すべての公開クラスがdポインタアプローチを採用しています。これが採用されていない唯一のケースは、そのクラスにメンバー変数が追加されることが決してないことが、前もってわかっている場合です。たとえば、QPointやQRectのようなクラスには、新しいメンバーが追加されることは想定されないので、データメンバーは、dポインタを用いずにクラス自体に直接保持されています。
Qtでは、すべての非公開オブジェクトは、QObjectPrivateをベースクラスとしていることに注意してください。
Q_DおよびQ_Q
上記のステップで行った最適化には、qポインタとdポインタがWidget型およびWidgetPrivate型となる副作用があります。つまり、下記のようなものは動作しません。
void Label::setText(const String &text) { // 動作しません!d_ptrはLabelPrivateオブジェクトをポイントするものの // WidgetPrivate型ではないためです d_ptr->text = text; }
したがって、dポインタに下層クラスでアクセスする場合には、適切な型へのstatic_castが必要となります。
void Label::setText(const String &text) { LabelPrivate *d = static_cast<LabelPrivate*>(d_ptr); // 自身の非公開型にキャストします d->text = text; }
ご覧のように、static_castをあらゆるところで見かけるのは美しくありません。代わりに以下のように、これを直感的にする2つのマクロがsrc/corelib/global/qglobal.hに定義されています。
global.h
#define Q_D(Class) Class##Private * const d = d_func() #define Q_Q(Class) Class * const q = q_func()
label.cpp
// Q_Dを使えば、LabelPrivateのメンバーをLabelから使用できます void Label::setText(const String &text) { Q_D(Label); d->text = text; } // Q_Qを使えば、LabelのメンバーをLabelPrivateから使用できます void LabelPrivate::someHelperFunction() { Q_Q(Label); q->selectAll(); }
Q_DECLARE_PRIVATEおよびQ_DECLARE_PUBLIC
Qtクラスには、公開クラス用にQ_DECLARE_PRIVATEマクロがあります。このマクロは以下のようになっています。
qglobal.h
#define Q_DECLARE_PRIVATE(Class)\ inline Class##Private* d_func() {\ return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr));\ }\ inline const Class##Private* d_func() const {\ return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr));\ }\ friend class Class##Private;
このマクロは以下のように使用することができます。
qlabel.h
class QLabel { private: Q_DECLARE_PRIVATE(QLabel) };
このアイディアは、QLabelが非公開内部クラスへのアクセスを許容するd_func()関数を提供するというものです。(マクロはqlabel.hの非公開セクションにありますので)メソッド自身は非公開です。しかしながら、d_func()は、QLabelのフレンド(C++フレンド)によって呼び出すことができるのです。これは、公開APIを使ってQLabel情報の一部にアクセスできないQtクラスが情報にアクセスするのに大変便利です。突飛な例として、ユーザーが何回リンクをクリックしたかをQLabelが記録しているものとしましょう。しかしながら、この情報にアクセスする公開APIはありません。QStatisticsはこの情報を必要としているクラスです。Qt開発者は、QStatisticsをQLabelのフレンドとして追加すれば、QStatisticsはlabel->d_func()->linkClickCountとすることができます。
d_funcは、正しいconstを推進する上で利点があります。MyClassのconstメンバー関数では、Q_D(const MyClass)を必要とするので、MyClassPrivateのconstメンバー関数しか呼び出せません。ナマのd_ptrを使えば、非const関数を呼び出すこともできます。