Come usare l’idioma PIMPL di Qt?

PIMPL è l’acronimo di P ointer to IMPL ementation. L’implementazione sta per “detail implementation”: qualcosa di cui gli utenti della class non devono preoccuparsi.

Le implementazioni di class propria di Qt separano in modo pulito le interfacce dalle implementazioni attraverso l’uso dell’idioma PIMPL. Tuttavia, i meccanismi forniti da Qt non sono documentati. Come usarli?

Mi piacerebbe che questa fosse la domanda canonica su “come faccio a PIMPL” in Qt. Le risposte devono essere motivate da una semplice interfaccia di dialogo per le voci di coordinazione mostrata di seguito.

La motivazione per l’uso di PIMPL diventa evidente quando abbiamo qualcosa con un’implementazione semi-complessa. Ulteriore motivazione è data in questa domanda . Anche una class abbastanza semplice deve inserire molte altre intestazioni nella sua interfaccia.

schermata di dialogo

L’interfaccia basata su PIMPL è abbastanza pulita e leggibile.

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

Un’interfaccia basata su Qt 5, C ++ 11 non ha bisogno della riga Q_PRIVATE_SLOT .

Confrontalo con un’interfaccia non PIMPL che ricopre i dettagli di implementazione nella sezione privata dell’interfaccia. Nota quanto altro codice deve essere incluso.

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

Queste due interfacce sono esattamente equivalenti per quanto riguarda la loro interfaccia pubblica. Hanno gli stessi segnali, slot e metodi pubblici.

introduzione

PIMPL è una class privata che contiene tutti i dati specifici dell’implementazione della class genitore. Qt fornisce un framework PIMPL e una serie di convenzioni che devono essere seguite quando si utilizza tale framework. I PIMPL di Qt possono essere utilizzati in tutte le classi, anche quelli non derivati ​​da QObject .

Il PIMPL deve essere allocato sull’heap. Nel linguaggio C ++ idiomatico, non dobbiamo gestire tale archiviazione manualmente, ma utilizzare un puntatore intelligente. O QScopedPointer o std::unique_ptr funzionano per questo scopo. Pertanto, una minima interfaccia basata su pimpl, non derivata da QObject , potrebbe essere simile a:

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

La dichiarazione del distruttore è necessaria, poiché il distruttore del puntatore dell’ambito ha bisogno di distruggere un’istanza di PIMPL. Il distruttore deve essere generato nel file di implementazione, in cui FooPrivate class FooPrivate :

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

Guarda anche:

  • Un’esposizione più profonda dell’idioma .
  • Gotchas e insidie ​​di PIMPL .

L’interfaccia

Spiegheremo ora l’interfaccia CoordinateDialog basata su PIMPL nella domanda.

Qt fornisce diverse macro e helper per l’implementazione che riducono la fatica di PIMPL. L’implementazione si aspetta che seguiamo queste regole:

  • Il PIMPL per una class Foo si chiama FooPrivate .
  • Il PIMPL viene inoltrato durante la dichiarazione della class Foo nel file dell’interfaccia (intestazione).

La macro Q_DECLARE_PRIVATE

La macro Q_DECLARE_PRIVATE deve essere inserita nella sezione private della dichiarazione della class. Prende il nome della class di interfaccia come parametro. Dichiara due implementazioni inline del metodo helper d_func() . Questo metodo restituisce il puntatore PIMPL con coerenza corretta. Quando viene utilizzato nei metodi const, restituisce un puntatore a un const PIMPL. Nei metodi non const, restituisce un puntatore a un PIMPL non costante. Fornisce anche un pimpl di tipo corretto nelle classi derivate. Ne consegue che tutti gli accessi al Pimpl dall’interno dell’implementazione devono essere eseguiti utilizzando d_func() e ** non tramite d_ptr . Di solito usiamo la macro Q_D , descritta nella sezione Implementazione di seguito.

La macro è disponibile in due versioni:

 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 

Nel nostro caso, Q_DECLARE_PRIAVATE(CoordinateDialog) è equivalente a Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog) .

La macro Q_PRIVATE_SLOT

Questa macro è necessaria solo per la compatibilità con Qt 4 o per il targeting di compilatori non C ++ 11. Per il codice Qt 5, C ++ 11, non è necessario, poiché è ansible connettere i funtori ai segnali e non è necessario disporre di slot privati ​​espliciti.

A volte abbiamo bisogno che QObject abbia slot privati ​​per uso interno. Tali slot inquinerebbero la sezione privata dell’interfaccia. Poiché le informazioni sugli slot sono rilevanti solo per il generatore di codici moc, possiamo invece utilizzare la macro Q_PRIVATE_SLOT per dire a Q_PRIVATE_SLOT che un determinato slot deve essere invocato tramite il puntatore d_func() , anziché attraverso this .

La syntax prevista da Q_PRIVATE_SLOT in Q_PRIVATE_SLOT è:

 Q_PRIVATE_SLOT(instance_pointer, method signature) 

Nel nostro caso:

 Q_PRIVATE_SLOT(d_func(), void onAccepted()) 

Ciò dichiara in modo efficace uno slot onAccepted nella class CoordinateDialog . Il moc genera il seguente codice per richiamare lo slot:

 d_func()->onAccepted() 

La macro stessa ha un’espansione vuota – fornisce solo informazioni a moc.

La nostra class di interfaccia è così ampliata come segue:

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

Quando si utilizza questa macro, è necessario includere il codice generato da Moc in una posizione in cui la class privata è completamente definita. Nel nostro caso, ciò significa che il file CoordinateDialog.cpp dovrebbe terminare con:

 #include "moc_CoordinateDialog.cpp" 

gotchas

  • Tutte le macro Q_ che devono essere utilizzate in una dichiarazione di class includono già un punto e virgola. Non sono necessari punti e virgola espliciti dopo 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 non deve essere una class privata all’interno di Foo stesso:

     // correct // wrong class FooPrivate; class Foo { class Foo { class FooPrivate; ... ... }; }; 
  • La prima sezione dopo la parentesi aperta in una dichiarazione di class è privata per impostazione predefinita. Quindi i seguenti sono equivalenti:

     // less wordy, preferred // verbose class Foo { class Foo { int privateMember; private: int privateMember; }; }; 
  • Il Q_DECLARE_PRIVATE aspetta il nome della class di interfaccia, non il nome di PIMPL:

     // correct // wrong class Foo { class Foo { Q_DECLARE_PRIVATE(Foo) Q_DECLARE_PRIVATE(FooPrivate) ... ... }; }; 
  • Il puntatore PIMPL deve essere const per le classi non copiabili / non assegnabili come QObject . Può essere non-const quando si implementano classi copiabili.

  • Poiché PIMPL è un dettaglio di implementazione interno, la sua dimensione non è disponibile nel sito in cui viene utilizzata l’interfaccia. La tentazione di utilizzare il posizionamento nuovo e l’idioma di Fast Pimpl dovrebbe essere contrastata in quanto non offre alcun vantaggio se non una class che non assegna affatto memoria.

L’implemento

Il PIMPL deve essere definito nel file di implementazione. Se è grande, può anche essere definito in un’intestazione privata, denominata abitualmente foo_p.h per una class la cui interfaccia è in foo.h

Il PIMPL, come minimo, è semplicemente un vettore dei dati della class principale. Ha solo bisogno di un costruttore e nessun altro metodo. Nel nostro caso, ha anche bisogno di memorizzare il puntatore nella class principale, poiché vorremmo emettere un segnale dalla class principale. Così:

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

PIMPL non è copiabile. Poiché utilizziamo membri non copiabili, qualsiasi tentativo di copiare o assegnare a PIMPL verrà catturato dal compilatore. In genere, è meglio distriggersre esplicitamente la funzionalità di copia utilizzando Q_DISABLE_COPY .

La macro Q_DECLARE_PUBLIC funziona in modo simile a Q_DECLARE_PRIVATE . È descritto più avanti in questa sezione.

Passiamo il puntatore alla finestra di dialogo nel costruttore, permettendoci di inizializzare il layout nella finestra di dialogo. Colleghiamo anche il segnale accettato di onAccepted slot interno 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 } 

Il onAccepted() deve essere esposto come uno slot nei progetti Qt 4 / non-C ++ 11. Per Qt 5 e C ++ 11, questo non è più necessario.

Accettando la finestra di dialogo, acquisiamo le coordinate ed emettiamo il segnale acceptedCoordinates . Ecco perché abbiamo bisogno del puntatore pubblico:

 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 dichiara una variabile locale CoordinateDialog * const q . È descritto più avanti in questa sezione.

La parte pubblica dell’implementazione costruisce PIMPL e ne espone le proprietà:

 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 dichiara una variabile CoordinateDialogPrivate * const d Q_D CoordinateDialogPrivate * const d locale. È descritto di seguito.

La macro Q_D

Per accedere a PIMPL in un metodo di interfaccia , possiamo usare la macro Q_D , passandole il nome della class di interfaccia.

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

Per accedere a PIMPL in un metodo di interfaccia const , è necessario anteporre il nome della class alla parola chiave const :

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

La macro Q_Q

Per accedere all’istanza dell’interfaccia da un metodo Q_Q non const , possiamo usare la macro Q_Q , passandogli il nome della class di interfaccia.

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

Per accedere all’istanza dell’interfaccia in un metodo PIMPL const , anteponiamo il nome della class con la parola chiave const , proprio come abbiamo fatto per 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

Questa macro è opzionale e viene utilizzata per consentire l’accesso all’interfaccia da PIMPL. Viene in genere usato se i metodi PIMPL devono manipolare la class base dell’interfaccia, o emettere i suoi segnali. La macro Q_DECLARE_PRIVATE equivalente è stata utilizzata per consentire l’accesso a PIMPL dall’interfaccia.

La macro accetta il nome della class di interfaccia come parametro. Dichiara due implementazioni inline del metodo helper q_func() . Questo metodo restituisce il puntatore dell’interfaccia con una coerenza corretta. Quando viene utilizzato nei metodi const, restituisce un puntatore a un’interfaccia const . Nei metodi non-const, restituisce un puntatore a un’interfaccia non-const. Fornisce inoltre l’interfaccia del tipo corretto nelle classi derivate. Ne consegue che tutti gli accessi all’interfaccia dall’interno di PIMPL devono essere eseguiti utilizzando q_func() e ** non tramite q_ptr . Di solito usiamo la macro Q_Q , descritta sopra.

La macro si aspetta che il puntatore all’interfaccia sia denominato q_ptr . Non esiste una variante a due argomenti di questa macro che consentirebbe di scegliere un nome diverso per il puntatore dell’interfaccia (come nel caso di Q_DECLARE_PRIVATE ).

La macro si espande come segue:

 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

Questa macro elimina il costruttore di copie e l’operatore di assegnazione. Deve apparire nella sezione privata di PIMPL.

Gotchas comuni

  • L’intestazione dell’interfaccia per una data class deve essere la prima intestazione da includere nel file di implementazione. Ciò impone che l’intestazione sia autonoma e non dipendente dalle dichiarazioni che sono state incluse nell’implementazione. In caso contrario, l’implementazione non riuscirà a compilare, consentendoti di correggere l’interfaccia per renderla autosufficiente.

     // 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 deve apparire nella sezione privata di PIMPL

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

Classi copiabili PIMPL e non QObject

L’idioma PIMPL consente di implementare oggetti copiabili, copiabili e spostabili, assegnabili. Il compito viene svolto attraverso l’idioma copy-and-swap , impedendo la duplicazione del codice. Il puntatore PIMPL non deve essere const, ovviamente.

Ricordiamo il in C ++ 11, abbiamo bisogno di prestare attenzione alla Regola dei Quattro e fornire tutto quanto segue: il costruttore di copia, il costruttore di movimento, l’operatore di assegnazione e il distruttore. E la funzione di swap per implementare tutto, ovviamente †.

Lo illustreremo utilizzando un esempio piuttosto inutile, ma comunque corretto.

Interfaccia

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

Per prestazioni, il costruttore di spostamenti e l’operatore di assegnazione devono essere definiti nel file di interfaccia (intestazione). Non è necessario accedere direttamente a PIMPL:

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

Tutti usano la funzione di swap , che dobbiamo definire anche nell’interfaccia. Si noti che lo è

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

Implementazione

Questo è piuttosto semplice. Non abbiamo bisogno di accedere all’interfaccia da Q_DECLARE_PUBLIC , quindi Q_DECLARE_PUBLIC e q_ptr sono assenti.

 // 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 questa eccellente risposta : Ci sono altre affermazioni che dovremmo specializzare std::swap per il nostro tipo, fornire uno swap in- swap -class insieme a uno swap free-function, ecc. Ma questo non è necessario: qualsiasi uso appropriato di swap sarà attraverso una chiamata non qualificata, e la nostra funzione sarà trovata tramite ADL . Una funzione lo farà.