Comment utiliser l’idiome PIMPL de Qt?

PIMPL signifie P ointer to IMPL ementation. L’implémentation est synonyme de “détail de l’implémentation”: quelque chose dont les utilisateurs de la classe n’ont pas besoin de se préoccuper.

Les propres implémentations de classes de Qt séparent proprement les interfaces des implémentations grâce à l’utilisation de l’idiome PIMPL. Cependant, les mécanismes fournis par Qt ne sont pas documentés. Comment les utiliser?

Je voudrais que ce soit la question canonique sur “comment faire PIMPL” dans Qt. Les réponses doivent être motivées par une simple interface de dialog 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 autre motivation est donnée dans cette question . Même une classe assez simple doit intégrer beaucoup d’autres en-têtes dans son interface.

capture d'écran de dialogue

L’interface basée sur PIMPL est assez propre et lisible.

// CoordinateDialog.h #include  #include  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 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 répartit les détails d’implémentation dans la section privée de l’interface. Notez combien d’autres codes doivent être inclus.

 // CoordinateDialog.h #include  #include  #include  #include  #include  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, créneaux horaires et méthodes publiques.

introduction

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 à suivre lors de l’utilisation de ce framework. Les PIMPL de Qt peuvent être utilisés dans toutes les classes, même celles qui ne sont pas dérivées de QObject .

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

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

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

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

Voir également:

  • Une exposition plus profonde de l’idiome .
  • Gotchas et pièges de PIMPL .

L’interface

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

Qt fournit plusieurs macros et aides à l’implémentation qui réduisent la pénibilité des PIMPL. L’implémentation attend de nous que nous suivions ces règles:

  • Le PIMPL pour une classe Foo s’appelle FooPrivate .
  • Le PIMPL est déclaré en avant 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 en tant que 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 la constance appropriée. Utilisé dans les méthodes const, il renvoie un pointeur sur un const PIMPL. Dans les méthodes non-const, il renvoie un pointeur sur un PIMPL non-const. Il fournit également un pimpl de type correct dans les classes dérivées. Il s’ensuit que tout access à pimpl depuis l’implémentation doit être effectué en utilisant d_func() et ** pas via d_ptr . Nous utilisons Q_D macro Q_D , décrite dans la section Implémentation ci-dessous.

La macro se décline 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é Qt 4 ou lors du ciblage de compilateurs non C ++ 11. Pour le code Qt 5, C ++ 11, il n’est pas nécessaire, car nous pouvons connecter des foncteurs à des signaux et vous n’avez pas besoin d’emplacements privés explicites.

Nous avons parfois besoin d’un QObject pour avoir des slots privés à usage interne. De tels emplacements pollueraient la section privée de l’interface. Étant donné que les informations sur les slots 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 d_func() via le pointeur d_func() , et non via this .

La syntaxe attendue par moc dans Q_PRIVATE_SLOT est la suivante:

 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 l’emplacement:

 d_func()->onAccepted() 

La macro elle-même a une extension vide – elle ne fournit que des informations à moc.

Notre classe d’interface est donc étendue 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(qGetPtrHelper(d_ptr)); } inline const CoordinateDialogPrivate* d_func() const { return reinterpret_cast(qGetPtrHelper(d_ptr)); } friend class CoordinateDialogPrivate; // Q_PRIVATE_SLOT(d_func(), void onAccepted()) // (empty) QScopedPointer const d_ptr; public: [...] }; 

Lorsque vous utilisez cette macro, vous devez inclure le code généré par moc dans un emplacement 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_ à utiliser dans une déclaration de classe comprennent 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 dans Foo 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ée par défaut. Ainsi, les éléments suivants sont équivalents:

     // less wordy, preferred // verbose class Foo { class Foo { int privateMember; private: int privateMember; }; }; 
  • 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 PIMPL est un détail d’implémentation interne, sa taille n’est pas disponible sur le site où l’interface est utilisée. Il faut résister à la tentation d’utiliser un placement nouveau et l’idiome de Fast Pimpl, car il n’offre aucun avantage, sauf pour 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 volumineux, il peut également être défini dans un en-tête privé, habituellement appelé foo_p.h pour une classe dont l’interface se trouve dans foo.h

La PIMPL, au minimum, n’est que le support des données de la classe principale. Il ne nécessite qu’un constructeur et 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. Ainsi:

 // CordinateDialog.cpp #include  #include  #include  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. Étant donné que nous utilisons des membres non copiables, toute tentative de copie ou d’atsortingbution 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 sur le dialog dans le constructeur, ce qui nous permet d’initialiser la mise en page dans la boîte de dialog. Nous connectons également le signal accepté par onAccepted connecteur 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 } 

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

Lors de l’acceptation du dialog, 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 à PIMPL dans une méthode d’ 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 à la méthode PIMPL dans une méthode d’ interface const , il faut append 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 transmettant 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 la 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 permet d’accéder à l’ interface depuis PIMPL. Il est généralement utilisé si les méthodes de 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 autoriser l’access à 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’assistance q_func() . Cette méthode renvoie le pointeur d’interface avec une constance correcte. Utilisé dans les méthodes const, il renvoie un pointeur sur une interface const . Dans les méthodes non const, il renvoie un pointeur sur une interface non-const. Il fournit également l’interface de type correct dans les classes dérivées. Il s’ensuit que tout access à l’interface depuis PIMPL doit être effectué en utilisant q_func() et ** pas via q_ptr . Nous utilisons Q_Q macro Q_Q , décrite ci-dessus.

La macro attend que le pointeur sur 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(q_ptr); } inline const CoordinateDialog* q_func() const { return static_cast(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ître dans la section privée du PIMPL.

Gotchas communs

  • L’en-tête d’ interface d’une classe donnée doit être le premier en-tête à être inclus dans le fichier d’implémentation. Cela force l’en-tête à être autonome et ne dépend pas des déclarations qui sont incluses dans l’implémentation. Si ce n’est pas le cas, l’implémentation ne pourra pas être compilée, ce qui vous permettra de réparer l’interface pour la rendre autonome.

     // correct // error prone // Foo.cpp // Foo.cpp #include "Foo.h" #include  #include  #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 PIMPL et non-QObject pouvant être copiées

L’identifiant PIMPL permet d’implémenter un object assignable, reproductible, copiable et déplaçable. L’affectation se fait à l’aide de l’idiome de copie et d’échange , empêchant la duplication du code. Le pointeur PIMPL ne doit évidemment pas être const.

Rappelez-vous le en C ++ 11, nous devons tenir compte de la règle de quatre et fournir tout ce qui suit: le constructeur de copie, le constructeur de déplacement, l’opérateur d’affectation et le destructeur. Et la fonction de swap autonome pour tout mettre en œuvre, bien sûr †.

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

Interface

 // Integer.h #include  class IntegerPrivate; class Integer { Q_DECLARE_PRIVATE(Integer) QScopedPointer 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 à PIMPL:

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

Toutes ces fonctions utilisent la fonction swap freestanding, que nous devons également définir dans l’interface. Notez que c’est

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

la mise en oeuvre

C’est plutôt simple. Nous n’avons pas besoin d’accéder à l’interface depuis PIMPL, ainsi 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 affirmations selon lesquelles nous devrions spécialiser std::swap pour notre type, fournir un swap classe le long d’un swap fonction libre, etc. Mais tout cela n’est pas nécessaire: toute utilisation correcte du swap sera à travers un appel sans réserve, et notre fonction sera trouvée par ADL . Une fonction fera l’affaire.