PIMPLは[〜#〜] p [〜#〜]ointer to[〜#〜] impl [ 〜#〜]セメンテーション。実装は「実装の詳細」の略で、クラスのユーザーが気にする必要のないものです。
Qt独自のクラス実装では、PIMPLイディオムを使用して、インターフェイスを実装から明確に分離しています。しかし、Qtが提供するメカニズムは文書化されていません。それらの使用方法は?
これをQtの「How do I PIMPL」に関する標準的な質問にしたいと思います。答えは、以下に示す単純な座標入力ダイアログインターフェイスによって動機付けられます。
PIMPLを使用する動機は、半複雑な実装で何かがある場合に明らかになります。さらなる動機は この質問 で与えられます。かなり単純なクラスでさえ、そのインターフェイスに他の多くのヘッダーを取り込む必要があります。
PIMPLベースのインターフェイスはかなりきれいで読みやすいです。
// CoordinateDialog.h
#include <QDialog>
#include <QVector3D>
class CoordinateDialogPrivate;
class CoordinateDialog : public QDialog
{
Q_OBJECT
Q_DECLARE_PRIVATE(CoordinateDialog)
#if QT_VERSION <= QT_VERSION_CHECK(5,0,0)
Q_PRIVATE_SLOT(d_func(), void onAccepted())
#endif
QScopedPointer<CoordinateDialogPrivate> const d_ptr;
public:
CoordinateDialog(QWidget * parent = 0, Qt::WindowFlags flags = 0);
~CoordinateDialog();
QVector3D coordinates() const;
Q_SIGNAL void acceptedCoordinates(const QVector3D &);
};
Q_DECLARE_METATYPE(QVector3D)
Qt 5、C++ 11ベースのインターフェースはQ_PRIVATE_SLOT
行を必要としません。
これを、実装の詳細をインターフェイスのプライベートセクションに組み込む非PIMPLインターフェイスと比較してください。他のコードをどれだけ含める必要があるかに注意してください。
// CoordinateDialog.h
#include <QDialog>
#include <QVector3D>
#include <QFormLayout>
#include <QDoubleSpinBox>
#include <QDialogButtonBox>
class CoordinateDialog : public QDialog
{
QFormLayout m_layout;
QDoubleSpinBox m_x, m_y, m_z;
QVector3D m_coordinates;
QDialogButtonBox m_buttons;
Q_SLOT void onAccepted();
public:
CoordinateDialog(QWidget * parent = 0, Qt::WindowFlags flags = 0);
QVector3D coordinates() const;
Q_SIGNAL void acceptedCoordinates(const QVector3D &);
};
Q_DECLARE_METATYPE(QVector3D)
これらの2つのインターフェイスは、パブリックインターフェイスに関する限り完全に同等です。それらは同じシグナル、スロット、パブリックメソッドを持っています。
PIMPLは、親クラスの実装固有のデータをすべて含むプライベートクラスです。 Qtは、PIMPLフレームワークと、そのフレームワークを使用する際に従う必要がある一連の規則を提供します。 QtのPIMPLは、QObject
から派生していないクラスも含め、すべてのクラスで使用できます。
PIMPLは、ヒープに割り当てる必要があります。慣用的なC++では、このようなストレージを手動で管理するのではなく、スマートポインターを使用する必要があります。 QScopedPointer
または_std::unique_ptr
_のいずれかがこの目的で機能します。したがって、QObject
から派生したものではなく、最小限のpimplベースのインターフェイスは次のようになります。
_// Foo.h
#include <QScopedPointer>
class FooPrivate; ///< The PIMPL class for Foo
class Foo {
QScopedPointer<FooPrivate> const d_ptr;
public:
Foo();
~Foo();
};
_
スコープ付きポインタのデストラクタはPIMPLのインスタンスを破壊する必要があるため、デストラクタの宣言が必要です。デストラクタは、FooPrivate
クラスが存在する実装ファイルで生成する必要があります。
_// Foo.cpp
class FooPrivate { };
Foo::Foo() : d_ptr(new FooPrivate) {}
Foo::~Foo() {}
_
こちらもご覧ください:
ここで、PIMPLベースのCoordinateDialog
インターフェースについて質問で説明します。
Qtは、PIMPLの面倒さを軽減するいくつかのマクロと実装ヘルパーを提供します。実装では、次の規則に従うことが期待されています。
Foo
のPIMPLの名前はFooPrivate
です。Foo
クラスの宣言に沿って前方宣言されます。_Q_DECLARE_PRIVATE
_マクロは、クラスの宣言のprivate
セクションに配置する必要があります。インターフェイスクラスの名前をパラメーターとして受け取ります。 d_func()
ヘルパーメソッドの2つのインライン実装を宣言します。そのメソッドは、適切なconstnessを持つPIMPLポインターを返します。 constメソッドで使用すると、const PIMPLへのポインターを返します。非constメソッドでは、非const PIMPLへのポインターを返します。また、派生クラスに正しい型のピンプルを提供します。実装内からのpimplへのすべてのアクセスは、d_func()
を使用して行われ、**(d_ptr
_を介さずに)行われることになります。通常、以下の実装セクションで説明する_Q_D
_マクロを使用します。
マクロには2つのフレーバーがあります。
_Q_DECLARE_PRIVATE(Class) // assumes that the PIMPL pointer is named d_ptr
Q_DECLARE_PRIVATE_D(Dptr, Class) // takes the PIMPL pointer name explicitly
_
この場合、Q_DECLARE_PRIAVATE(CoordinateDialog)
はQ_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog)
と同等です。
このマクロは、Qt 4との互換性のため、または非C++ 11コンパイラを対象とする場合にのみ必要です。 Qt 5、C++ 11コードの場合、ファンクターをシグナルに接続でき、明示的なプライベートスロットは必要ないため、不要です。
内部使用のためにQObject
にプライベートスロットが必要になる場合があります。このようなスロットは、インターフェイスのプライベートセクションを汚染します。スロットに関する情報はmocコードジェネレーターにのみ関連するため、代わりに_Q_PRIVATE_SLOT
_マクロを使用して、代わりにd_func()
ポインターを介して特定のスロットが呼び出されることをmocに伝えることができます。からthis
まで。
_Q_PRIVATE_SLOT
_のmocが期待する構文は次のとおりです。
_Q_PRIVATE_SLOT(instance_pointer, method signature)
_
私たちの場合には:
_Q_PRIVATE_SLOT(d_func(), void onAccepted())
_
これはonAccepted
クラスでCoordinateDialog
スロットを効果的に宣言します。 mocは、スロットを呼び出す次のコードを生成します。
_d_func()->onAccepted()
_
マクロ自体には空の展開があります-mocに情報を提供するだけです。
したがって、インターフェイスクラスは次のように拡張されます。
_class CoordinateDialog : public QDialog
{
Q_OBJECT /* We don't expand it here as it's off-topic. */
// Q_DECLARE_PRIVATE(CoordinateDialog)
inline CoordinateDialogPrivate* d_func() {
return reinterpret_cast<CoordinateDialogPrivate *>(qGetPtrHelper(d_ptr));
}
inline const CoordinateDialogPrivate* d_func() const {
return reinterpret_cast<const CoordinateDialogPrivate *>(qGetPtrHelper(d_ptr));
}
friend class CoordinateDialogPrivate;
// Q_PRIVATE_SLOT(d_func(), void onAccepted())
// (empty)
QScopedPointer<CoordinateDialogPrivate> const d_ptr;
public:
[...]
};
_
このマクロを使用する場合、プライベートクラスが完全に定義されている場所にmocで生成されたコードを含める必要があります。私たちの場合、これは_CoordinateDialog.cpp
_ファイルがendで次のようになっていることを意味します:
_#include "moc_CoordinateDialog.cpp"
_
クラス宣言で使用される_Q_
_マクロにはすべて、セミコロンが既に含まれています。 _Q_
_の後に明示的なセミコロンは必要ありません。
_// correct // verbose, has double semicolons
class Foo : public QObject { class Foo : public QObject {
Q_OBJECT Q_OBJECT;
Q_DECLARE_PRIVATE(...) Q_DECLARE_PRIVATE(...);
... ...
}; };
_
PIMPL する必要はありませんFoo
自体のプライベートクラスである:
_// correct // wrong
class FooPrivate; class Foo {
class Foo { class FooPrivate;
... ...
}; };
_
クラス宣言の左中括弧の後の最初のセクションは、デフォルトではプライベートです。したがって、以下は同等です。
_// less wordy, preferred // verbose
class Foo { class Foo {
int privateMember; private:
int privateMember;
}; };
_
_Q_DECLARE_PRIVATE
_には、PIMPLの名前ではなく、インターフェイスクラスの名前が必要です。
_// correct // wrong
class Foo { class Foo {
Q_DECLARE_PRIVATE(Foo) Q_DECLARE_PRIVATE(FooPrivate)
... ...
}; };
_
PIMPLポインターは、QObject
などのコピー不可/割り当て不可のクラスのconstである必要があります。コピー可能なクラスを実装する場合、非constにすることができます。
PIMPLは内部実装の詳細であるため、そのサイズは、インターフェイスが使用されるサイトでは利用できません。プレースメントnewと Fast Pimpl イディオムを使用する誘惑は、メモリをまったく割り当てないクラス以外には何の利点も提供しないため、抵抗する必要があります。
PIMPLは実装ファイルで定義する必要があります。大きい場合は、プライベートヘッダーで定義することもできます。通常、インターフェイスが_foo_p.h
_にあるクラスの_foo.h
_という名前になります。
PIMPLは、少なくとも、メインクラスのデータの単なるキャリアです。コンストラクタのみが必要で、他のメソッドは不要です。私たちの場合、メインクラスからシグナルを発信したいので、メインクラスへのポインタも保存する必要があります。したがって:
_// CordinateDialog.cpp
#include <QFormLayout>
#include <QDoubleSpinBox>
#include <QDialogButtonBox>
class CoordinateDialogPrivate {
Q_DISABLE_COPY(CoordinateDialogPrivate)
Q_DECLARE_PUBLIC(CoordinateDialog)
CoordinateDialog * const q_ptr;
QFormLayout layout;
QDoubleSpinBox x, y, z;
QDialogButtonBox buttons;
QVector3D coordinates;
void onAccepted();
CoordinateDialogPrivate(CoordinateDialog*);
};
_
PIMPLはコピーできません。コピー不可のメンバーを使用しているため、PIMPLにコピーまたは割り当てを試みると、コンパイラーによってキャッチされます。通常、_Q_DISABLE_COPY
_を使用して、コピー機能を明示的に無効にするのが最善です。
_Q_DECLARE_PUBLIC
_マクロは_Q_DECLARE_PRIVATE
_と同様に機能します。このセクションの後半で説明します。
ダイアログへのポインタをコンストラクタに渡し、ダイアログのレイアウトを初期化できるようにします。また、QDialog
の受け入れられた信号を内部onAccepted
スロットに接続します。
_CoordinateDialogPrivate::CoordinateDialogPrivate(CoordinateDialog * dialog) :
q_ptr(dialog),
layout(dialog),
buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel)
{
layout.addRow("X", &x);
layout.addRow("Y", &y);
layout.addRow("Z", &z);
layout.addRow(&buttons);
dialog->connect(&buttons, SIGNAL(accepted()), SLOT(accept()));
dialog->connect(&buttons, SIGNAL(rejected()), SLOT(reject()));
#if QT_VERSION <= QT_VERSION_CHECK(5,0,0)
this->connect(dialog, SIGNAL(accepted()), SLOT(onAccepted()));
#else
QObject::connect(dialog, &QDialog::accepted, [this]{ onAccepted(); });
#endif
}
_
onAccepted()
PIMPLメソッドは、Qt 4 /非C++ 11プロジェクトのスロットとして公開する必要があります。 Qt 5およびC++ 11では、これはもう必要ありません。
ダイアログが受け入れられると、座標をキャプチャしてacceptedCoordinates
信号を発信します。そのため、パブリックポインターが必要です。
_void CoordinateDialogPrivate::onAccepted() {
Q_Q(CoordinateDialog);
coordinates.setX(x.value());
coordinates.setY(y.value());
coordinates.setZ(z.value());
emit q->acceptedCoordinates(coordinates);
}
_
_Q_Q
_マクロは、ローカルの_CoordinateDialog * const q
_変数を宣言します。このセクションの後半で説明します。
実装の公開部分はPIMPLを構築し、そのプロパティを公開します:
_CoordinateDialog::CoordinateDialog(QWidget * parent, Qt::WindowFlags flags) :
QDialog(parent, flags),
d_ptr(new CoordinateDialogPrivate(this))
{}
QVector3D CoordinateDialog::coordinates() const {
Q_D(const CoordinateDialog);
return d->coordinates;
}
CoordinateDialog::~CoordinateDialog() {}
_
_Q_D
_マクロは、ローカルの_CoordinateDialogPrivate * const d
_変数を宣言します。以下に説明します。
interfaceメソッドでPIMPLにアクセスするには、_Q_D
_マクロを使用して、インターフェイスクラスの名前を渡します。
_void Class::foo() /* non-const */ {
Q_D(Class); /* needs a semicolon! */
// expands to
ClassPrivate * const d = d_func();
...
_
const interfaceメソッドでPIMPLにアクセスするには、クラス名の前にconst
キーワードを追加する必要があります。
_void Class::bar() const {
Q_D(const Class);
// expands to
const ClassPrivate * const d = d_func();
...
_
non-const PIMPLメソッドからインターフェイスインスタンスにアクセスするには、_Q_Q
_マクロを使用して、インターフェイスクラスの名前を渡します。
_void ClassPrivate::foo() /* non-const*/ {
Q_Q(Class); /* needs a semicolon! */
// expands to
Class * const q = q_func();
...
_
const PIMPLメソッドでインターフェイスインスタンスにアクセスするには、_Q_D
_マクロの場合と同様に、const
キーワードをクラス名の先頭に追加します。
_void ClassPrivate::foo() const {
Q_Q(const Class); /* needs a semicolon! */
// expands to
const Class * const q = q_func();
...
_
このマクロはオプションであり、PIMPLからinterfaceへのアクセスを許可するために使用されます。通常、PIMPLのメソッドがインターフェイスの基本クラスを操作したり、その信号を送信する必要がある場合に使用されます。同等の_Q_DECLARE_PRIVATE
_マクロを使用して、インターフェイスから[〜#〜] pimpl [〜#〜]へのアクセスを許可しました。
マクロは、インターフェイスクラスの名前をパラメーターとして受け取ります。 q_func()
ヘルパーメソッドの2つのインライン実装を宣言します。そのメソッドは、適切なconstnessを持つインターフェイスポインターを返します。 constメソッドで使用すると、constインターフェイスへのポインターを返します。非constメソッドでは、非constインターフェイスへのポインターを返します。また、派生クラスで正しい型のインターフェイスを提供します。 PIMPL内からインターフェイスへのすべてのアクセスは、q_func()
を使用して行われ、**(q_ptr
_を介してではなく)行われることになります。通常、上記の_Q_Q
_マクロを使用します。
マクロは、_q_ptr
_という名前のインターフェイスへのポインターを想定しています。 (_Q_DECLARE_PRIVATE
_の場合のように)インターフェイスポインターに別の名前を選択できる、このマクロの2つの引数のバリエーションはありません。
マクロは次のように展開されます。
_class CoordinateDialogPrivate {
//Q_DECLARE_PUBLIC(CoordinateDialog)
inline CoordinateDialog* q_func() {
return static_cast<CoordinateDialog*>(q_ptr);
}
inline const CoordinateDialog* q_func() const {
return static_cast<const CoordinateDialog*>(q_ptr);
}
friend class CoordinateDialog;
//
CoordinateDialog * const q_ptr;
...
};
_
このマクロは、コピーコンストラクターと代入演算子を削除します。 must PIMPLのprivateセクションに表示されます。
特定のクラスのinterfaceヘッダーは、実装ファイルに含まれる最初のヘッダーでなければなりません。これにより、ヘッダーは自己完結型になり、実装に含まれる宣言に依存しなくなります。そうでない場合、実装はコンパイルに失敗し、インターフェイスを修正して自給自足にすることができます。
_// correct // error prone
// Foo.cpp // Foo.cpp
#include "Foo.h" #include <SomethingElse>
#include <SomethingElse> #include "Foo.h"
// Now "Foo.h" can depend on SomethingElse without
// us being aware of the fact.
_
_Q_DISABLE_COPY
_マクロは、PIMPLのprivateセクションに表示する必要があります
_// correct // wrong
// Foo.cpp // Foo.cpp
class FooPrivate { class FooPrivate {
Q_DISABLE_COPY(FooPrivate) public:
... Q_DISABLE_COPY(FooPrivate)
}; ...
};
_
PIMPLのイディオムにより、コピー可能、コピーおよび移動構築可能な割り当て可能なオブジェクトを実装できます。割り当ては copy-and-swap イディオムによって行われ、コードの重複を防ぎます。もちろん、PIMPLポインターはconstであってはなりません。
C++ 11では、 Rule of Four に注意し、次のallを提供する必要があります:コピーコンストラクター、移動コンストラクター、代入演算子、およびデストラクター。そしてもちろん、すべてを実装するための独立したswap
関数です†。
むしろ役に立たないが、それでも正しい例を使用してこれを説明します。
_// Integer.h
#include <algorithm>
class IntegerPrivate;
class Integer {
Q_DECLARE_PRIVATE(Integer)
QScopedPointer<IntegerPrivate> d_ptr;
public:
Integer();
Integer(int);
Integer(const Integer & other);
Integer(Integer && other);
operator int&();
operator int() const;
Integer & operator=(Integer other);
friend void swap(Integer& first, Integer& second) /* nothrow */;
~Integer();
};
_
パフォーマンスのために、移動コンストラクターと割り当て演算子は、インターフェース(ヘッダー)ファイルで定義する必要があります。 PIMPLに直接アクセスする必要はありません。
_Integer::Integer(Integer && other) : Integer() {
swap(*this, other);
}
Integer & Integer::operator=(Integer other) {
swap(*this, other);
return *this;
}
_
これらはすべて、swap
独立関数を使用します。これは、インターフェイスでも定義する必要があります。ことに注意してください
_void swap(Integer& first, Integer& second) /* nothrow */ {
using std::swap;
swap(first.d_ptr, second.d_ptr);
}
_
これはかなり簡単です。 PIMPLからインターフェイスにアクセスする必要がないため、_Q_DECLARE_PUBLIC
_および_q_ptr
_はありません。
_// Integer.cpp
class IntegerPrivate {
public:
int value;
IntegerPrivate(int i) : value(i) {}
};
Integer::Integer() : d_ptr(new IntegerPrivate(0)) {}
Integer::Integer(int i) : d_ptr(new IntegerPrivate(i)) {}
Integer::Integer(const Integer &other) :
d_ptr(new IntegerPrivate(other.d_func()->value)) {}
Integer::operator int&() { return d_func()->value; }
Integer::operator int() const { return d_func()->value; }
Integer::~Integer() {}
_
†Per この優れた答え :型に_std::swap
_を特化し、クラス内のswap
をフリー関数swap
など。ただし、これはすべて不要です。swap
の適切な使用は、修飾されていない呼び出しを通じて行われ、関数は [〜#〜] adl [〜# 〜] 。 1つの機能で十分です。