D-Pointer/ja: Difference between revisions
Deleted-869 (talk | contribs) (Draft #6) |
Deleted-869 (talk | contribs) (Draft #7) |
||
| Line 95: | Line 95: | ||
WidgetLib 1.0では、ラベルのテキストメンバは(論理的)オフセット1の場所にあります。コンパイラがアプリケーション中に生成したコードでは、<tt>Label::text()</tt>メソッドは、アプリケーション中のラベルオブジェクトから1だけオフセットした位置にアクセスするものと翻訳されるのです。しかし、WidgetLib 1.1では、ラベルの'''テキスト'''メンバは(論理的)オフセット2にシフトしてしまっています!アプリケーションは、再コンパイルされていないため<tt>text</tt>がオフセット1にあるものと考えていますので、<tt>stylesheet</tt>変数にアクセスしてしまうのです! | WidgetLib 1.0では、ラベルのテキストメンバは(論理的)オフセット1の場所にあります。コンパイラがアプリケーション中に生成したコードでは、<tt>Label::text()</tt>メソッドは、アプリケーション中のラベルオブジェクトから1だけオフセットした位置にアクセスするものと翻訳されるのです。しかし、WidgetLib 1.1では、ラベルの'''テキスト'''メンバは(論理的)オフセット2にシフトしてしまっています!アプリケーションは、再コンパイルされていないため<tt>text</tt>がオフセット1にあるものと考えていますので、<tt>stylesheet</tt>変数にアクセスしてしまうのです! | ||
ここで、なぜ<code>Label::text()</code>のオフセット計算コードがCuteAppバイナリで終わってしまって、WidgetLibバイナリで終わらないのかと不思議に思う人がいるはずです。その答えは、<code>Label::text()</code>用のコードがヘッダファイルで定義されているので、コンパイラがそれを[https://ja.wikipedia.org/wiki/インライン関数 インライン化]するだけで終わってしまうからです。 | |||
それでは、<code>Label::text()</code>がインライン化されていなければ事情は変わるのでしょうか。つまり、<code>Label::text()</code>をソースファイルに移動したとしたら?答えはノーです。C++コンパイラは、オブジェクトのサイズがコンパイル時と実行時で同じであることに依存しています。たとえば、スタックワインディング・アンワインディングがそうです。もしラベルオブジェクトをスタックに作成したとすると、コンパイラはスタックに領域を割り当てるのに、コンパイル時のラベルサイズを用いるコードを生成します。WidgetLib 1.1では、実行時のラベルサイズが違うために、ラベルのコンストラクタは既存のスタックデータを上書きしてしまい、スタックの破損をもたらすのです。 | |||
===エクスポートされたC++クラスのサイズは決して変更してはならない=== | |||
=== | 要約すると、あなたのライブラリが一旦リリースされたならば、(ユーザーに見える形で)<u>エクスポートされた</u>C++クラスのサイズやレイアウト(データを動かし回ってはいけません)は、けっして変更してはなりません。C++コンパイラは、クラス中のデータのサイズや順序は、アプリケーションがコンパイルされた<u>後は</u>変更されないと仮定してコードを生成します。 | ||
すると、どうやって、オブジェクトのサイズを変更せずに、新機能を追加できるでしょうか。 | |||
==dポインタ== | ==dポインタ== | ||
Revision as of 12:18, 30 November 2023
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変数にアクセスしてしまうのです!
ここで、なぜ<code>Label::text()</code>のオフセット計算コードがCuteAppバイナリで終わってしまって、WidgetLibバイナリで終わらないのかと不思議に思う人がいるはずです。その答えは、<code>Label::text()</code>用のコードがヘッダファイルで定義されているので、コンパイラがそれをインライン化するだけで終わってしまうからです。
それでは、<code>Label::text()</code>がインライン化されていなければ事情は変わるのでしょうか。つまり、<code>Label::text()</code>をソースファイルに移動したとしたら?答えはノーです。C++コンパイラは、オブジェクトのサイズがコンパイル時と実行時で同じであることに依存しています。たとえば、スタックワインディング・アンワインディングがそうです。もしラベルオブジェクトをスタックに作成したとすると、コンパイラはスタックに領域を割り当てるのに、コンパイル時のラベルサイズを用いるコードを生成します。WidgetLib 1.1では、実行時のラベルサイズが違うために、ラベルのコンストラクタは既存のスタックデータを上書きしてしまい、スタックの破損をもたらすのです。
エクスポートされたC++クラスのサイズは決して変更してはならない
要約すると、あなたのライブラリが一旦リリースされたならば、(ユーザーに見える形で)エクスポートされたC++クラスのサイズやレイアウト(データを動かし回ってはいけません)は、けっして変更してはなりません。C++コンパイラは、クラス中のデータのサイズや順序は、アプリケーションがコンパイルされた後は変更されないと仮定してコードを生成します。
すると、どうやって、オブジェクトのサイズを変更せずに、新機能を追加できるでしょうか。
dポインタ
ここで上手い方法は、単一のポインタのみを保管することで、ライブラリのすべての公開クラスのサイズを一定に保つ方法です。このポインタは、全てのデータを含む非公開/内部データ構造体をポイントしています。この内部構造体のサイズは、アプリケーションにどんな副作用も及ぼすことなく、縮んだり大きくなったりすることができます。なぜなら、ポインタはライブラリコード内のみでアクセスされ、アプリケーションの視点からは、オブジェクトのサイズは、つねにポインタのサイズであり、けっして変化しないからです。このポインタがdポインタと呼ばれます。
このパターンの精神は、下記のコードにまとめられています(この記事のコードはすべてデストラクタを持っていませんが、もちろん実際のコードではこれらを追加しなくてはなりません)。
widget.h
/* Since d_ptr is a pointer and is never referended in header file
(it would cause a compile error) WidgetPrivate doesn't have to be included,
but forward-declared instead.
The definition of the class can be written in widget.cpp or
in a separate file, say widget_p.h */
class WidgetPrivate;
class Widget
{
// ...
Rect geometry() const;
// ...
private:
WidgetPrivate *d_ptr;
};
widget_p.h:これはウィジェットクラスの非公開ヘッダファイルです。
/* widget_p.h (_p means private) */
struct WidgetPrivate
{
Rect geometry;
String stylesheet;
};
widget.cpp
// With this #include, we can access WidgetPrivate.
#include "widget_p.h"
Widget::Widget() : d_ptr(new WidgetPrivate)
{
// Creation of private data
}
Rect Widget::geometry() const
{
// The d-ptr is only accessed in the library code
return d_ptr->geometry;
}
つぎに、ウィジェットに基づいた子クラスの例です。
label.h
class Label : public Widget
{
// ...
String text();
private:
// Each class maintains its own d-pointer
LabelPrivate *d_ptr;
};
label.cpp
// Unlike WidgetPrivate, the author decided LabelPrivate
// to be defined in the source file itself
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
{
// Constructor that initializes the q-ptr
WidgetPrivate(Widget *q) : q_ptr(q) { }
Widget *q_ptr; // q-ptr points to the API class
Rect geometry;
String stylesheet;
};
widget.cpp
#include "widget_p.h"
// Create private data.
// Pass the 'this' pointer to initialize the q-ptr
Widget::Widget() : d_ptr(new WidgetPrivate(this))
{
}
Rect Widget::geometry() const
{
// the d-ptr is only accessed in the library code
return d_ptr->geometry;
}
以下は、ウィジェットに基づいた別のクラスです。
label.h
class Label : public Widget
{
// ...
String text() const;
private:
LabelPrivate *d_ptr;
};
label.cpp
// Unlike WidgetPrivate, the author decided LabelPrivate
// to be defined in the source file itself
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:
// only subclasses may access the below
// allow subclasses to initialize with their own concrete Private
Widget(WidgetPrivate &d);
WidgetPrivate *d_ptr;
};
widget_p.h
struct WidgetPrivate
{
WidgetPrivate(Widget *q) : q_ptr(q) { } // constructor that initializes the q-ptr
Widget *q_ptr; // q-ptr that points to the API class
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); // allow Label subclasses to pass on their Private
// notice how Label does not have a d_ptr! It just uses Widget's d_ptr.
};
label.cpp
#include "widget_p.h"
class LabelPrivate : public WidgetPrivate
{
public:
String text;
};
Label::Label()
: Widget(*new LabelPrivate) // initialize the d-pointer with our own Private
{
}
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)
{
// won't work! since d_ptr is of type WidgetPrivate even though
// it points to LabelPrivate object
d_ptr->text = text;
}
したがって、dポインタに下層クラスでアクセスする場合には、適切な型へのstatic_castが必要となります。
void Label::setText(const String &text)
{
LabelPrivate *d = static_cast<LabelPrivate*>(d_ptr); // cast to our private type
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
// With Q_D you can use the members of LabelPrivate from Label
void Label::setText(const String &text)
{
Q_D(Label);
d->text = text;
}
// With Q_Q you can use the members of Label from 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)
};
The idea is that QLabel provides a function d_func() that allows access to its private internal class. The method itself is private (since the macro is inside a private section in qlabel.h). The d_func() can however be invoked by friends (C++ friend) of QLabel. This is primarily useful for access of information by Qt classes which cannot get access of some QLabel information using public api. As a bizarre example, QLabel might keep track of how many times the user has clicked on a link. However, there is no public API to access this information. QStatistics is a class that needs this information. A Qt developer will add QStatistics as a friend of QLabel and QStatistics can then do label->d_func()->linkClickCount.
The d_func also has the advantage to enforce const-correctness: In a const member function of MyClass you need a Q_D(const MyClass) and thus you can only call const member functions in MyClassPrivate. With a naked d_ptr you could also call non-const functions.