web-dev-qa-db-fra.com

Comment utiliser l'idiome PIMPL de Qt?

PIMPL signifie [~ # ~] p [~ # ~] ointer to [~ # ~] impl [ ~ # ~] ementation. L'implémentation signifie "détail d'implémentation": quelque chose que les utilisateurs de la classe n'ont pas à se soucier.

Les propres implémentations de classe de Qt séparent proprement 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 que ce soit la question canonique sur "comment puis-je PIMPL" dans Qt. Les réponses doivent être motivées par une simple interface de dialogue de saisie de coordonnées illustrée ci-dessous.

La motivation pour l'utilisation de PIMPL devient évidente lorsque nous avons quelque chose avec une implémentation semi-complexe. Une motivation supplémentaire est donnée dans cette question . Même une classe assez simple doit insérer de nombreux autres en-têtes 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 Q_PRIVATE_SLOT ligne.

Comparez cela à une interface non-PIMPL qui place les détails d'implémentation dans la section privée de l'interface. Notez combien d'autre 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, emplacements et méthodes publiques.

50
Kuba Ober

Introduction

Le PIMPL est une classe privée qui contient toutes les données spécifiques à l'implémentation de la classe parente. Qt fournit un cadre PIMPL et un ensemble de conventions qui doivent être suivies lors de l'utilisation de ce cadre. Les PIMPL de Qt peuvent être utilisés dans toutes les classes, même celles non dérivées de QObject.

Le PIMPL doit être alloué sur le tas. En C++ idiomatique, nous ne devons pas gérer ce stockage manuellement, mais utiliser un pointeur intelligent. QScopedPointer ou std::unique_ptr Fonctionnent à 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();
};

La déclaration du destructeur est nécessaire, car le destructeur du pointeur de portée doit détruire une instance de PIMPL. Le destructeur doit être généré dans le fichier d'implémentation, où réside la classe FooPrivate:

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

Voir également:

L'interface

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

Qt fournit plusieurs macros et aides à l'implémentation qui réduisent la corvée des PIMPL. La mise en œuvre attend de nous que nous suivions ces règles:

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

La macro Q_DECLARE_PRIVATE

La macro Q_DECLARE_PRIVATE Doit être placée dans la section private de la déclaration de la classe. Il prend le nom de la classe d'interface comme paramètre. Il déclare deux implémentations en ligne de la méthode d'assistance d_func(). Cette méthode renvoie le pointeur PIMPL avec une constance appropriée. Lorsqu'il est utilisé dans les méthodes const, il renvoie un pointeur sur un const PIMPL. Dans les méthodes non const, il renvoie un pointeur vers un PIMPL non const. Il fournit également un bouton de type correct dans les classes dérivées. Il s'ensuit que tout accès au pimpl à partir de l'implémentation doit être fait en utilisant d_func() et ** pas par d_ptr. Habituellement, nous utilisons la macro Q_D, Décrite dans la section Implémentation ci-dessous.

La macro est disponible en deux versions:

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é avec Qt 4 ou lors du ciblage de compilateurs non C++ 11. Pour Qt 5, le code C++ 11, ce n'est pas nécessaire, car nous pouvons connecter des foncteurs aux signaux et il n'y a pas besoin d'emplacements privés explicites.

Nous avons parfois besoin d'un QObject pour avoir des emplacements privés à usage interne. De tels emplacements pollueraient la section privée de l'interface. Étant donné que les informations sur les emplacements ne concernent que le générateur de code moc, nous pouvons utiliser la macro Q_PRIVATE_SLOT Pour indiquer à moc qu'un emplacement donné doit être appelé via le pointeur d_func(), au lieu de 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())

Cela déclare effectivement un emplacement onAccepted sur la classe CoordinateDialog. Le moc génère le code suivant pour appeler le slot:

d_func()->onAccepted()

La macro elle-même a une extension vide - elle ne fournit que 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 généré par moc à un endroit où la classe privée est entièrement définie. Dans notre cas, cela signifie que le fichier CoordinateDialog.cpp Doit se terminer par:

#include "moc_CoordinateDialog.cpp"

Gotchas

  • Toutes les macros Q_ Qui doivent être utilisées dans une déclaration de classe incluent déjà 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(...);
      ...                              ...
    };                               };
    
  • Le PIMPL ne doit pas être une classe privée au sein de Foo lui-même:

    // correct                  // wrong
    class FooPrivate;           class Foo {
    class Foo {                   class FooPrivate;
      ...                         ...
    };                          };
    
  • La première section après l'accolade ouvrante dans une déclaration de classe est privée par défaut. Ainsi, les éléments 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 attribuables telles que QObject. Il peut être non-const lors de l'implémentation de classes copiables.

  • Le PIMPL étant un détail d'implémentation interne, sa taille n'est pas disponible sur le site où l'interface est utilisée. La tentation d'utiliser le placement new et l'idiome Fast Pimpl doit être résistée car elle n'offre aucun avantage à part une classe qui n'alloue pas de mémoire du tout.

La mise en oeuvre

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

Le PIMPL, au minimum, est simplement un support des données de la classe principale. Il n'a besoin que d'un constructeur et d'aucune autre méthode. Dans notre cas, il doit également stocker le pointeur sur la classe principale, car nous voulons émettre un signal de la classe principale. Donc:

// 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 copie ou d'assignation au PIMPL serait interceptée par le compilateur. En règle générale, 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 manière similaire à Q_DECLARE_PRIVATE. Il est décrit plus loin dans cette section.

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

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

Après acceptation de la boîte de 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 méthode interface, nous pouvons utiliser la macro Q_D En lui passant le nom de la classe d'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, nous devons ajouter le nom de la 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 d'une méthode PIMPL non-const, 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 d'interface dans une méthode const PIMPL, nous ajoutons le nom de classe avec le mot clé const, comme nous l'avons fait pour la macro Q_D:

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

La macro Q_DECLARE_PUBLIC

Cette macro est facultative et est utilisée pour autoriser l'accès à interface à partir du PIMPL. Il est généralement utilisé si les méthodes du PIMPL doivent manipuler la classe de base de l'interface ou émettre ses signaux. La macro Q_DECLARE_PRIVATE Équivalente a été utilisée pour permettre l'accès au pimpl [~ # ~] [~ # ~] depuis 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'assistance q_func(). Cette méthode renvoie le pointeur d'interface avec une 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 fournit également l'interface de 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 ** pas par q_ptr. Habituellement, nous utilisons la macro Q_Q Décrite ci-dessus.

La macro s'attend à ce que le pointeur vers 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. Il doit apparaît dans la section privée du PIMPL.

Gotchas communs

  • L'en-tête interface pour une classe donnée doit être le premier en-tête à être inclus dans le fichier d'implémentation. Cela oblige l'en-tête à être autonome et à ne pas dépendre des déclarations qui se trouvent être incluses dans l'implémentation. Si ce n'est pas le cas, l'implémentation échouera à la compilation, 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 un objet assignable copiable, copiable et déplaçable. L'affectation se fait via l'idiome copy-and-swap , empêchant la duplication de code. Le pointeur PIMPL ne doit pas être const, bien sûr.

Rappelez-vous en C++ 11, nous devons tenir compte de la règle de quatre , et fournir tout des éléments suivants: le constructeur de copie, le constructeur de déplacement, l'opérateur d'affectation, et destructeur. Et la fonction autonome swap pour implémenter tout cela, 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 les performances, le constructeur de déplacement et l'opérateur d'affectation doivent être définis dans le fichier d'interface (en-tête). 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-ci utilisent la fonction autonome swap, que nous devons également définir dans l'interface. Notez qu'il est

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

La mise en oeuvre

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

† Per cette excellente réponse : Il y a d'autres revendications selon lesquelles nous devrions spécialiser std::swap Pour notre type, fournir un swap en classe avec une fonction libre swap, etc. Mais tout cela n'est pas nécessaire: toute utilisation appropriée de swap se fera par le biais d'un appel non qualifié, et notre fonction sera trouvée via ADL . Une fonction fera l'affaire.

88
Kuba Ober