web-dev-qa-db-ja.com

QtのPIMPLイディオムの使用方法は?

PIMPLは[〜#〜] p [〜#〜]ointer to[〜#〜] impl [ 〜#〜]セメンテーション。実装は「実装の詳細」の略で、クラスのユーザーが気にする必要のないものです。

Qt独自のクラス実装では、PIMPLイディオムを使用して、インターフェイスを実装から明確に分離しています。しかし、Qtが提供するメカニズムは文書化されていません。それらの使用方法は?

これをQtの「How do I PIMPL」に関する標準的な質問にしたいと思います。答えは、以下に示す単純な座標入力ダイアログインターフェイスによって動機付けられます。

PIMPLを使用する動機は、半複雑な実装で何かがある場合に明らかになります。さらなる動機は この質問 で与えられます。かなり単純なクラスでさえ、そのインターフェイスに他の多くのヘッダーを取り込む必要があります。

dialog screenshot

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つのインターフェイスは、パブリックインターフェイスに関する限り完全に同等です。それらは同じシグナル、スロット、パブリックメソッドを持っています。

50
Kuba Ober

前書き

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です。
  • PIMPLは、インターフェース(ヘッダー)ファイルのFooクラスの宣言に沿って前方宣言されます。

Q_DECLARE_PRIVATEマクロ

_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)と同等です。

Q_PRIVATE_SLOTマクロ

このマクロは、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_変数を宣言します。以下に説明します。

Q_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();
  ...
_

Q_Qマクロ

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();
  ...
_

Q_DECLARE_PUBLICマクロ

このマクロはオプションであり、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;
  ...
};
_

Q_DISABLE_COPYマクロ

このマクロは、コピーコンストラクターと代入演算子を削除します。 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および非QObjectのコピー可能なクラス

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つの機能で十分です。

88
Kuba Ober