Comment utiliser L'idiome PIMPL du Qt?

PIMPL signifie P ointer à IMPL ementation. L'implémentation signifie "détail d'implémentation": quelque chose dont les utilisateurs de la classe n'ont pas besoin de se préoccuper.

les propres implémentations de classe de Qt séparent clairement les interfaces des implémentations grâce à L'utilisation de L'idiome PIMPL. Pourtant, les mécanismes fournis par Qt ne sont pas documentés. Comment les utiliser?

j'aimerais c'est la question canonique de "how do I PIMPL" dans Qt. Les réponses doivent être motivées par une simple interface de dialogue d'entrée de coordonnées ci-dessous.

la motivation pour l'utilisation de PIMPL devient évidente lorsque nous avons quelque chose avec une mise en œuvre semi-complexe. Une autre motivation est donnée dans cette question . Même une classe assez simple doit tirer dans beaucoup d'autres headers dans son interface.

dialog screenshot

l'interface basée sur PIMPL est assez propre et lisible.

// 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)

une interface basée sur Qt 5, C++11 n'a pas besoin de la ligne Q_PRIVATE_SLOT .

comparez cela à une interface non-PIMPL qui Tuc les détails de mise en œuvre dans la section privée de l'interface. Remarque combien les autres code doit être inclus.

// 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)

ces deux interfaces sont exactement équivalentes en ce qui concerne leur interface publique. Ils ont les mêmes signaux, les fentes et les méthodes publiques.

46
demandé sur Community 2014-08-11 22:39:23

1 réponses

Introduction

le PIMPL est une classe privée qui contient toutes les données spécifiques à la mise en œuvre de la classe mère. Qt fournit un cadre PIMPL et un ensemble de conventions qui doivent être suivies lors de l'utilisation de ce cadre. Qt's PIMPLs peut être utilisé dans toutes les classes, même ceux qui ne sont pas dérivés de QObject .

Le PIMPL doit être alloué sur le tas. En C++ idiomatique, nous ne devons pas gérer un tel stockage manuellement, mais utiliser un pointeur intelligent. Soit QScopedPointer ou std::unique_ptr travaux à cet effet. Ainsi, une interface minimale basée sur pimpl, non dérivée de QObject , pourrait ressembler à:

// Foo.h
#include <QScopedPointer>
class FooPrivate; ///< The PIMPL class for Foo
class Foo {
  QScopedPointer<FooPrivate> const d_ptr;
public:
  Foo();
  ~Foo();
};

Le destructeur de la déclaration est nécessaire, étant donné l'étendue du pointeur destructeur doit détruire une instance de la PIMPL. Le destructeur doit être généré dans le fichier de mise en œuvre, où la classe FooPrivate vit:

// Foo.cpp
class FooPrivate { };
Foo::Foo() : d_ptr(new FooPrivate) {}
Foo::~Foo() {}

voir aussi:

L'Interface

nous allons maintenant expliquer l'interface CoordinateDialog basée sur PIMPL dans la question.

Qt fournit plusieurs macros et des aides à la mise en œuvre qui réduisent la pénibilité des boutons. Le implementation attend de nous que nous suivions ces règles:

  • le bouton D'une classe Foo est nommé FooPrivate .
  • le PIMPL est forward-déclaré le long de la déclaration de la classe Foo dans le fichier d'interface (en-tête).

the Q_DECLARE_PRIVATE Macro

la macro Q_DECLARE_PRIVATE doit figurer dans la section private de la déclaration de la classe. Il prend le nom de la classe de l'interface comme paramètre. Il déclare deux implémentations en ligne de la méthode d'aide d_func() . Cette méthode renvoie le pointeur PIMPL avec la constance appropriée. Utilisé dans les méthodes const, il renvoie un pointeur vers un const PIMPL. Dans les méthodes non-const, il renvoie un pointeur vers un PIMPL non-const. Il fournit également un pimpl de type correct dans les classes dérivées. Il s'ensuit que tout accès au pimpl à partir de la mise en œuvre doit être fait en utilisant d_func() et **non par d_ptr . Habituellement, nous utilisons la macro Q_D , décrite dans la section Mise en œuvre ci-dessous.

la macro est en deux saveurs:

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

Dans notre cas, Q_DECLARE_PRIAVATE(CoordinateDialog) est équivalent à Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog) .

la Macro Q_PRIVATE_SLOT

cette macro n'est nécessaire que pour la compatibilité Qt 4, ou lorsqu'elle cible des compilateurs non-C++11. Pour Qt 5, Code C++11, c'est inutile, car nous pouvons connecter les foncteurs aux signaux et il n'y a pas besoin de slots privés explicites.

nous avons parfois besoin d'un QObject pour avoir des emplacements privés à usage interne. De tels slots pollueraient la section privée de l'interface. Puisque les informations sur les slots ne concernent que le générateur de code moc, nous pouvons, au lieu de cela, utiliser la macro Q_PRIVATE_SLOT pour dire à moc qu'un slot donné doit être invoqué à travers le pointeur d_func() , à la place de par this .

la syntaxe attendue par moc dans le Q_PRIVATE_SLOT est:

Q_PRIVATE_SLOT(instance_pointer, method signature)

dans notre cas:

Q_PRIVATE_SLOT(d_func(), void onAccepted())

ceci déclare effectivement une fente onAccepted sur la classe CoordinateDialog . Le moc génère le code suivant pour invoquer la fente:

d_func()->onAccepted()

la macro elle - même a une extension vide-elle fournit seulement des informations à moc.

Notre classe d'interface est ainsi développée comme suit:

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:
  [...]
};

lorsque vous utilisez cette macro, vous devez inclure le code produit par moc dans un endroit où la classe privée est entièrement définie. Dans notre cas, cela signifie que le fichier CoordinateDialog.cpp devrait fin avec:

#include "moc_CoordinateDialog.cpp"

Pièges

  • toutes les macros Q_ qui doivent être utilisées dans une déclaration de classe déjà inclure un point-virgule. Aucun point-virgule explicite n'est nécessaire après 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 ne doit pas être privé de la classe dans Foo lui-même:

    // correct                  // wrong
    class FooPrivate;           class Foo {
    class Foo {                   class FooPrivate;
      ...                         ...
    };                          };
    
  • La première section après l'accolade d'ouverture dans une déclaration de classe est privé par défaut. Par conséquent, les termes suivants sont équivalents:

    // less wordy, preferred    // verbose
    class Foo {                 class Foo {              
      int privateMember;        private:
                                  int privateMember;
    };                          };
    
  • le Q_DECLARE_PRIVATE attend le nom de la classe d'interface, pas le nom du PIMPL:

    // correct                  // wrong
    class Foo {                 class Foo {
      Q_DECLARE_PRIVATE(Foo)      Q_DECLARE_PRIVATE(FooPrivate)
      ...                         ...
    };                          };
    
  • le pointeur PIMPL doit être const pour les classes non copiables/Non assignables telles que QObject . Il peut être non-const lors de l'implémentation de classes copiables.

  • étant donné que le PIMPL est un détail interne de mise en œuvre, sa taille n'est pas disponible sur le site où le l'interface est utilisée. Il faut résister à la tentation d'utiliser le nouveau placement et l'idiome Fast Pimpl car il ne fournit aucun avantage pour autre chose qu'une classe qui n'alloue pas de mémoire du tout.

La Mise En Œuvre

le PIMPL doit être défini dans le fichier d'implémentation. S'il est grand, il peut aussi être défini dans un en-tête privé, habituellement appelé foo_p.h pour une classe dont l'interface est dans foo.h .

le PIMPL, au minimum, n'est qu'un support des données de la classe principale. Il a seulement besoin d'un constructeur et pas d'autres méthodes. Dans notre cas, il doit également stocker le pointeur vers la classe principale, car nous voulons émettre un signal de la classe principale. Ainsi:

// 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*);
};

Le PIMPL n'est pas copiable. Puisque nous utilisons des membres non copiables, toute tentative de copier ou d'assigner au PIMPL serait prise par le compilateur. En général, il est préférable de désactiver explicitement la fonctionnalité de copie en utilisant Q_DISABLE_COPY .

la macro Q_DECLARE_PUBLIC fonctionne de la même manière que Q_DECLARE_PRIVATE . Il est décrit plus loin dans cette section.

nous passons le pointeur vers la boîte de dialogue dans le constructeur, ce qui nous permet d'initialiser la mise en page sur la boîte de dialogue. Nous connectons également le signal accepté du QDialog à la fente interne 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
}

le La méthode PIMPL onAccepted() doit être exposée comme une fente dans les projets Qt 4/non-C++11. Pour Qt 5 et C++11, cela n'est plus nécessaire.

après acceptation du dialogue, nous capturons les coordonnées et émettons le signal acceptedCoordinates . C'est pourquoi nous avons besoin du pointeur public:

void CoordinateDialogPrivate::onAccepted() {
  Q_Q(CoordinateDialog);
  coordinates.setX(x.value());
  coordinates.setY(y.value());
  coordinates.setZ(z.value());
  emit q->acceptedCoordinates(coordinates);
}

la macro Q_Q déclare une variable locale CoordinateDialog * const q . Il est décrit plus loin dans cette section.

La partie publique de l'implémentation construit le PIMPL et expose ses propriétés:

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() {}

la macro Q_D déclare une variable locale CoordinateDialogPrivate * const d . Il est décrit ci-dessous.

la Macro Q_D

pour accéder au PIMPL dans une interface méthode, nous pouvons utiliser la macro Q_D , en lui passant le nom de la classe interface.

void Class::foo() /* non-const */ {
  Q_D(Class);    /* needs a semicolon! */
  // expands to
  ClassPrivate * const d = d_func();
  ...

pour accéder au PIMPL dans une méthode const interface , nous devons préparer le nom de classe avec le mot-clé const :

void Class::bar() const {
  Q_D(const Class);
  // expands to
  const ClassPrivate * const d = d_func();
  ...

la Macro Q_Q

pour accéder à l'instance d'interface à partir de la méthode non-const PIMPL , nous pouvons utiliser la macro Q_Q , en lui passant le nom de la classe d'interface.

void ClassPrivate::foo() /* non-const*/ {
  Q_Q(Class);   /* needs a semicolon! */
  // expands to
  Class * const q = q_func();
  ...

pour accéder à l'instance de l'interface dans un const PIMPL méthode, nous préparons le nom de classe avec le mot-clé const , tout comme nous l'avons fait pour le Q_D macro:

void ClassPrivate::foo() const {
  Q_Q(const Class);   /* needs a semicolon! */
  // expands to
  const Class * const q = q_func();
  ...

the Q_DECLARE_PUBLIC Macro

cette macro est optionnelle et est utilisée pour permettre l'accès à interface à partir du PIMPL. Il est généralement utilisé si les méthodes du PIMPL ont besoin de manipuler la classe de base de l'interface, ou d'émettre ses signaux. L'équivalent Q_DECLARE_PRIVATE macro a été utilisé pour permettre l'accès au PIMPL à partir de l'interface.

la macro prend le nom de la classe d'interface comme paramètre. Il déclare deux implémentations en ligne de la méthode d'aide q_func() . Cette méthode renvoie le pointeur d'interface avec la constance appropriée. Lorsqu'il est utilisé dans les méthodes const, il renvoie un pointeur vers une interface const . Dans les méthodes non-const, il renvoie un pointeur vers une interface non-const. Il a également fournit l'interface du type correct dans les classes dérivées. Il s'ensuit que tout accès à l'interface à partir du PIMPL doit être fait en utilisant q_func() et **et non par q_ptr . Habituellement, nous utilisons la macro Q_Q , décrite ci-dessus.

la macro s'attend à ce que le pointeur de l'interface soit nommé q_ptr . Il n'y a pas de Variante à deux arguments de cette macro qui permettrait de choisir un nom différent pour le pointeur d'interface (comme c'était le cas pour Q_DECLARE_PRIVATE ).

la macro se développe comme suit:

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;
  ...
};

la Macro Q_disable_copy

Cette macro supprime le constructeur de copie et l'opérateur d'affectation. doit apparaissent dans la section privée de la PIMPL.

Pièges Courants

  • Le interface header pour un la classe doit être le premier en-tête à inclure dans le fichier d'implémentation. Cela oblige l'en-tête à être autonome et ne dépend pas des déclarations qui se trouvent être inclus dans la mise en œuvre. Si ce n'est pas le cas, l'implémentation échouera à compiler, vous permettant de corriger l'interface pour la rendre autosuffisante.

    // 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.
    
  • la macro Q_DISABLE_COPY doit apparaître dans la section privée du PIMPL

    // correct                   // wrong
    // Foo.cpp                   // Foo.cpp
    
    class FooPrivate {           class FooPrivate {
      Q_DISABLE_COPY(FooPrivate) public:
      ...                          Q_DISABLE_COPY(FooPrivate)
    };                              ...
                                 };
    

classes copiables PIMPL et Non-QObject

l'idiome PIMPL permet d'implémenter des objets copiables, copiables - et move - constructibles, assignables. La cession se fait à travers l'idiome copy-and-swap , empêchant la duplication de code. Le pointeur PIMPL ne doit pas être const, bien sûr.

rappelle le dans C++11, nous devons tenir compte de la règle de quatre , et fournir tous des éléments suivants: le constructeur de copies, le constructeur de mouvements, l'opérateur d'affectation et le destructeur. Et la fonction autonome swap pour mettre en œuvre tout, bien sûr†.

nous allons illustrer cela en utilisant un exemple plutôt inutile, mais néanmoins correct.

Interface

// 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();
};

pour la performance, le constructeur de déplacement et l'opérateur d'affectation doivent être définis en le fichier d'interface (header). Ils n'ont pas besoin d'accéder directement au PIMPL:

Integer::Integer(Integer && other) : Integer() {
   swap(*this, other);
}

Integer & Integer::operator=(Integer other) {
   swap(*this, other);
   return *this;
}

tous ceux qui utilisent la fonction swap autonome, que nous devons définir dans l'interface aussi bien. Notez qu'il s'agit de

void swap(Integer& first, Integer& second) /* nothrow */ {
   using std::swap;
   swap(first.d_ptr, second.d_ptr);
}

mise en Œuvre

c'est assez simple. Nous n'avons pas besoin d'accéder à l'interface depuis le PIMPL, donc Q_DECLARE_PUBLIC et q_ptr sont absents.

// 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() {}

†par cette excellente réponse : il y a d'autres revendications que nous devrions spécialiser std::swap pour notre type, fournir un dans la classe swap le long d'une fonction libre swap , etc. Mais tout cela est inutile: toute utilisation correcte de swap sera par un appel sans réserve, et notre fonction sera trouvée par ADL . Une fonction ne se.

82
répondu Kuba Ober 2018-04-09 17:27:52