Como usar o idioma PIMPL do Qt?

PIMPL significa P ointer para IMPL ementation. A implementação significa “detalhes de implementação”: algo que os usuários da class não precisam se preocupar.

As implementações de class do próprio Qt separam claramente as interfaces das implementações através do uso do idioma PIMPL. No entanto, os mecanismos fornecidos pelo Qt são indocumentados. Como usá-los?

Eu gostaria que essa fosse a questão canônica sobre “como eu faço PIMPL” no Qt. As respostas devem ser motivadas por uma interface de diálogo de input de coordenadas simples mostrada abaixo.

A motivação para o uso do PIMPL se torna aparente quando temos algo com uma implementação semi-complexa. Mais motivação é dada nesta questão . Até mesmo uma class bastante simples tem que include muitos outros headers em sua interface.

captura de tela de diálogo

A interface baseada em PIMPL é razoavelmente limpa e legível.

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

Uma interface baseada em Qt 5, C ++ 11 não precisa da linha Q_PRIVATE_SLOT .

Compare isso com uma interface não-PIMPL que agrupa detalhes de implementação na seção privada da interface. Observe quanto outro código deve ser incluído.

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

Essas duas interfaces são exatamente equivalentes no que diz respeito à sua interface pública. Eles têm os mesmos sinais, slots e methods públicos.

Introdução

O PIMPL é uma class privada que contém todos os dados específicos da implementação da class pai. O Qt fornece uma estrutura PIMPL e um conjunto de convenções que precisam ser seguidas ao usar essa estrutura. Os PIMPLs do Qt podem ser usados ​​em todas as classs, mesmo aquelas não derivadas do QObject .

O PIMPL precisa ser alocado no heap. No idiomático C ++, não devemos gerenciar esse armazenamento manualmente, mas usar um ponteiro inteligente. QScopedPointer ou std::unique_ptr trabalham para essa finalidade. Assim, uma interface baseada em pimpl mínima, não derivada do QObject , pode se parecer com:

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

A declaração do destruidor é necessária, uma vez que o destrutor do ponteiro com escopo definido precisa destruir uma instância do PIMPL. O destruidor deve ser gerado no arquivo de implementação, onde a class FooPrivate vive:

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

Veja também:

  • Uma exposição mais profunda do idioma .
  • Pegadinhas e armadilhas do PIMPL .

A interface

Vamos agora explicar a interface CoordinateDialog baseada em PIMPL na questão.

O Qt fornece várias macros e ajudantes de implementação que reduzem o trabalho penoso dos PIMPLs. A implementação espera que sigamos estas regras:

  • O PIMPL para uma class Foo é chamado FooPrivate .
  • O PIMPL é declarado para a frente ao longo da declaração da class Foo no arquivo de interface (header).

A macro Q_DECLARE_PRIVATE

A macro Q_DECLARE_PRIVATE deve ser colocada na seção private da declaração da class. Leva o nome da class de interface como um parâmetro. Ele declara duas implementações em linha do método auxiliar d_func() . Esse método retorna o ponteiro PIMPL com constância adequada. Quando usado em methods const, ele retorna um ponteiro para um const PIMPL. Em methods não constantes, ele retorna um ponteiro para um PIMPL não const. Ele também fornece um pimpl do tipo correto em classs derivadas. Segue-se que todo o access ao pimpl de dentro da implementação deve ser feito usando d_func() e ** não através de d_ptr . Normalmente, usamos a macro Q_D , descrita na seção Implementação abaixo.

A macro vem em dois sabores:

 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 

No nosso caso, Q_DECLARE_PRIAVATE(CoordinateDialog) é equivalente a Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog) .

A macro Q_PRIVATE_SLOT

Essa macro é necessária apenas para compatibilidade com o Qt 4 ou ao segmentar compiladores não-C ++ 11. Para Qt 5, código C ++ 11, é desnecessário, já que podemos conectar functores a sinais e não há necessidade de slots privados explícitos.

Às vezes, precisamos que um QObject tenha slots privados para uso interno. Esses slots poluiriam a seção privada da interface. Como as informações sobre slots são relevantes apenas para o gerador de código moc, podemos, em vez disso, usar a macro Q_PRIVATE_SLOT para informar ao moc que um determinado slot deve ser invocado através do ponteiro d_func() , em vez de através dele.

A syntax esperada pelo moc no Q_PRIVATE_SLOT é:

 Q_PRIVATE_SLOT(instance_pointer, method signature) 

No nosso caso:

 Q_PRIVATE_SLOT(d_func(), void onAccepted()) 

Isso efetivamente declara um slot onAccepted na class CoordinateDialog . O moc gera o seguinte código para invocar o slot:

 d_func()->onAccepted() 

A macro em si tem uma expansão vazia – fornece apenas informações para o moc.

Nossa class de interface é assim expandida da seguinte maneira:

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

Ao usar essa macro, você deve include o código gerado pelo moc em um local onde a class privada esteja totalmente definida. No nosso caso, isso significa que o arquivo CoordinateDialog.cpp deve terminar com:

 #include "moc_CoordinateDialog.cpp" 

Gotchas

  • Todas as macros Q_ que devem ser usadas em uma declaração de class já incluem um ponto-e-vírgula. Nenhum ponto-e-vírgula explícito é necessário apó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(...); ... ... }; }; 
  • O PIMPL não deve ser uma class privada dentro do próprio Foo :

     // correct // wrong class FooPrivate; class Foo { class Foo { class FooPrivate; ... ... }; }; 
  • A primeira seção após a chave de abertura em uma declaração de class é privada por padrão. Assim, os seguintes são equivalentes:

     // less wordy, preferred // verbose class Foo { class Foo { int privateMember; private: int privateMember; }; }; 
  • O Q_DECLARE_PRIVATE espera o nome da class de interface, não o nome do PIMPL:

     // correct // wrong class Foo { class Foo { Q_DECLARE_PRIVATE(Foo) Q_DECLARE_PRIVATE(FooPrivate) ... ... }; }; 
  • O ponteiro PIMPL deve ser const para classs não copiáveis ​​/ não atribuíveis, como QObject . Pode ser não-constante ao implementar classs copiáveis.

  • Como o PIMPL é um detalhe de implementação interna, seu tamanho não está disponível no site em que a interface é usada. A tentação de usar o posicionamento novo e o idioma do Fast Pimpl deve ser evitada, já que não oferece benefícios para nada além de uma class que não aloque memory de forma alguma.

A implementação

O PIMPL tem que ser definido no arquivo de implementação. Se for grande, também pode ser definido em um header privado, geralmente chamado foo_p.h para uma class cuja interface esteja em foo.h

O PIMPL, no mínimo, é meramente um portador dos dados da class principal. Só precisa de um construtor e nenhum outro método. No nosso caso, ele também precisa armazenar o ponteiro para a class principal, pois queremos emitir um sinal da class principal. Portanto:

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

O PIMPL não é copiável. Como usamos membros não copiáveis, qualquer tentativa de copiar ou atribuir ao PIMPL seria capturada pelo compilador. Geralmente, é melhor desabilitar explicitamente a funcionalidade de cópia usando Q_DISABLE_COPY .

A macro Q_DECLARE_PUBLIC funciona de forma semelhante a Q_DECLARE_PRIVATE . É descrito mais adiante nesta seção.

Passamos o ponteiro para o diálogo no construtor, permitindo inicializar o layout no diálogo. Também conectamos o sinal aceito do QDialog ao slot onAccepted interno.

 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 } 

O método PIMPL onAccepted() precisa ser exposto como um slot nos projetos Qt 4 / non-C ++ 11. Para Qt 5 e C ++ 11, isso não é mais necessário.

Após a aceitação da checkbox de diálogo, capturamos as coordenadas e emitimos o sinal aceiteCoordinates. É por isso que precisamos do ponteiro público:

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

A macro Q_Q declara uma variável local CoordinateDialog * const q . É descrito mais adiante nesta seção.

A parte pública da implementação constrói o PIMPL e expõe suas propriedades:

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

A macro Q_D declara uma variável Q_D local CoordinateDialogPrivate * const d . É descrito abaixo.

A macro Q_D

Para acessar o PIMPL em um método de interface , podemos usar a macro Q_D , passando o nome da class de interface.

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

Para acessar o PIMPL em um método de interface const , precisamos prefixar o nome da class com a palavra-chave const :

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

A macro Q_Q

Para acessar a instância da interface de um método PIMPL não const , podemos usar a macro Q_Q , passando o nome da class de interface.

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

Para acessar a instância da interface em um método const PIMPL , prefixamos o nome da class com a palavra-chave const , assim como fizemos com a macro Q_D :

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

A macro Q_DECLARE_PUBLIC

Essa macro é opcional e é usada para permitir o access à interface do PIMPL. É normalmente usado se os methods do PIMPL precisarem manipular a class base da interface ou emitirem seus sinais. A macro Q_DECLARE_PRIVATE equivalente foi usada para permitir access ao PIMPL a partir da interface.

A macro usa o nome da class da interface como um parâmetro. Ele declara duas implementações em linha do método auxiliar q_func() . Esse método retorna o ponteiro da interface com a constância apropriada. Quando usado em methods const, ele retorna um ponteiro para uma interface const . Em methods não constantes, ele retorna um ponteiro para uma interface não-const. Ele também fornece a interface do tipo correto em classs derivadas. Segue-se que todo o access à interface de dentro do PIMPL deve ser feito usando q_func() e ** não através de q_ptr . Normalmente, usamos a macro Q_Q , descrita acima.

A macro espera que o ponteiro para a interface seja nomeado q_ptr . Não há uma variante de dois argumentos dessa macro que permita escolher um nome diferente para o ponteiro da interface (como era o caso de Q_DECLARE_PRIVATE ).

A macro se expande da seguinte maneira:

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

A macro Q_DISABLE_COPY

Essa macro exclui o construtor de cópia e o operador de atribuição. Deve aparecer na seção privada do PIMPL.

Gotchas Comuns

  • O header da interface para uma determinada class deve ser o primeiro header a ser incluído no arquivo de implementação. Isso força o header a ser autônomo e não dependente de declarações que são incluídas na implementação. Se não for assim, a implementação não será compilada, permitindo que você corrija a interface para torná-la auto-suficiente.

     // 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. 
  • A macro Q_DISABLE_COPY deve aparecer na seção privada do PIMPL

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

Classes copiáveis ​​PIMPL e não-QObject

O idioma PIMPL permite implementar objects copiáveis, que podem ser copiados e movidos, construtíveis e designáveis. A atribuição é feita através do idioma de cópia e troca , impedindo a duplicação de código. O ponteiro PIMPL não deve ser const, é claro.

Lembre-se de que, em C ++ 11, precisamos observar a Regra de Quatro e fornecer todos os itens a seguir: o construtor de cópia, o construtor de movimento, o operador de atribuição e o destruidor. E a function de swap independente para implementar tudo isso, é claro †.

Vamos ilustrar isso usando um exemplo bastante inútil, mas, no entanto, correto.

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

Para desempenho, o construtor de movimento e o operador de atribuição devem ser definidos no arquivo de interface (header). Eles não precisam acessar o PIMPL diretamente:

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

Todos os que usam a function swap autônoma, que devemos definir na interface também. Note que é

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

Implementação

Isso é bastante simples. Nós não precisamos de access à interface do PIMPL, assim Q_DECLARE_PUBLIC e q_ptr estão ausentes.

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

† Por essa excelente resposta : Há outras alegações de que devemos especializar o std::swap para o nosso tipo, fornecer uma swap dentro da class ao lado de uma swap function livre, etc. Mas isso é tudo desnecessário: qualquer uso adequado do swap será através de uma chamada não qualificada, e nossa function será encontrada através da ADL . Uma function serve.