¿Cómo usar el idioma PIMPL de Qt?

PIMPL significa P ointer to IMPL ementation. La implementación significa “detalle de implementación”: algo con lo que los usuarios de la clase no deben preocuparse.

Las implementaciones de clase propias de Qt separan claramente las interfaces de las implementaciones mediante el uso de la expresión idiomática PIMPL. Sin embargo, los mecanismos proporcionados por Qt no están documentados. Cómo usarlos?

Me gustaría que esta sea la pregunta canónica sobre “cómo hago PIMPL” en Qt. Las respuestas deben estar motivadas por una interfaz de diálogo de entrada de coordenadas simple que se muestra a continuación.

La motivación para el uso de PIMPL se hace evidente cuando tenemos algo con una implementación semi-compleja. Motivación adicional se da en esta pregunta . Incluso una clase bastante simple tiene que incorporar muchos otros encabezados en su interfaz.

captura de pantalla de diálogo

La interfaz basada en PIMPL es bastante limpia y legible.

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

Una interfaz basada en Qt 5, C ++ 11 no necesita la línea Q_PRIVATE_SLOT .

Compare eso con una interfaz que no sea PIMPL que meta detalles de implementación en la sección privada de la interfaz. Tenga en cuenta cuánto otro código tiene que ser incluido.

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

Esas dos interfaces son exactamente equivalentes en lo que respecta a su interfaz pública. Tienen las mismas señales, ranuras y métodos públicos.

Introducción

El PIMPL es una clase privada que contiene todos los datos específicos de la implementación de la clase principal. Qt proporciona un marco PIMPL y un conjunto de convenciones que deben seguirse al usar ese marco. Los PIMPL de Qt se pueden usar en todas las clases, incluso aquellas que no se derivan de QObject .

El PIMPL debe asignarse en el montón. En C ++ idiomático, no debemos administrar dicho almacenamiento manualmente, sino usar un puntero inteligente. O bien QScopedPointer o std::unique_ptr funcionan para este propósito. Por lo tanto, una interfaz mínima basada en pimpl, no derivada de QObject , podría verse así:

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

La statement del destructor es necesaria, ya que el destructor del puntero del scope necesita destruir una instancia del PIMPL. El destructor se debe generar en el archivo de implementación, donde vive la clase FooPrivate :

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

Ver también:

  • Una exposición más profunda de la expresión idiomática .
  • Las trampas y las trampas de PIMPL .

La interfaz

Ahora explicaremos la interfaz CoordinateDialog basada en PIMPL en la pregunta.

Qt proporciona varias macros y ayudantes de implementación que reducen la carga de trabajo de los PIMPL. La implementación espera que sigamos estas reglas:

  • El PIMPL para una clase Foo se llama FooPrivate .
  • El PIMPL se declara hacia adelante a lo largo de la statement de la clase Foo en el archivo de interfaz (encabezado).

La macro Q_DECLARE_PRIVATE

La macro Q_DECLARE_PRIVATE debe colocarse en la sección private de la statement de la clase. Toma el nombre de la clase de interfaz como un parámetro. Declara dos implementaciones en línea del método auxiliar d_func() . Ese método devuelve el puntero PIMPL con la constness adecuada. Cuando se usa en métodos const, devuelve un puntero a un const PIMPL. En métodos no const, devuelve un puntero a un PIMPL no const. También proporciona un pimpl del tipo correcto en las clases derivadas. Se deduce que todo acceso al pimpl desde la implementación se debe hacer usando d_func() y ** no a través de d_ptr . Normalmente usamos la macro Q_D , que se describe en la sección Implementación a continuación.

La macro viene en dos 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 

En nuestro caso, Q_DECLARE_PRIAVATE(CoordinateDialog) es equivalente a Q_DECLARE_PRIVATE_D(d_ptr, CoordinateDialog) .

La macro Q_PRIVATE_SLOT

Esta macro solo es necesaria para la compatibilidad con Qt 4 o cuando se dirige a comstackdores que no son C ++ 11. Para el código Qt 5, C ++ 11, no es necesario, ya que podemos conectar funtores a las señales y no hay necesidad de ranuras privadas explícitas.

A veces necesitamos un QObject para tener espacios privados para uso interno. Tales ranuras contaminarían la sección privada de la interfaz. Dado que la información sobre slots solo es relevante para el generador de código moc, podemos, en cambio, usar la macro Q_PRIVATE_SLOT para indicarle a moc que una ranura dada debe invocarse a través del puntero d_func() , en lugar de hacerlo a través de this .

La syntax esperada por moc en Q_PRIVATE_SLOT es:

 Q_PRIVATE_SLOT(instance_pointer, method signature) 

En nuestro caso:

 Q_PRIVATE_SLOT(d_func(), void onAccepted()) 

Esto declara efectivamente una ranura onAccepted en la clase CoordinateDialog . El moc genera el siguiente código para invocar el espacio:

 d_func()->onAccepted() 

La macro en sí tiene una expansión vacía: solo proporciona información a moc.

Nuestra clase de interfaz se expande de la siguiente manera:

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

Al utilizar esta macro, debe incluir el código generado por moc en un lugar donde la clase privada está completamente definida. En nuestro caso, esto significa que el archivo CoordinateDialog.cpp debe finalizar con:

 #include "moc_CoordinateDialog.cpp" 

Gotchas

  • Todas las macros Q_ que se usarán en una statement de clase ya incluyen un punto y coma. No se necesitan puntos y comas explícitos después de Q_ :

     // correct // verbose, has double semicolons class Foo : public QObject { class Foo : public QObject { Q_OBJECT Q_OBJECT; Q_DECLARE_PRIVATE(...) Q_DECLARE_PRIVATE(...); ... ... }; }; 
  • El PIMPL no debe ser una clase privada dentro de Foo :

     // correct // wrong class FooPrivate; class Foo { class Foo { class FooPrivate; ... ... }; }; 
  • La primera sección después de la llave de apertura en una statement de clase es privada por defecto. Por lo tanto, los siguientes son equivalentes:

     // less wordy, preferred // verbose class Foo { class Foo { int privateMember; private: int privateMember; }; }; 
  • El Q_DECLARE_PRIVATE espera el nombre de la clase de interfaz, no el nombre de PIMPL:

     // correct // wrong class Foo { class Foo { Q_DECLARE_PRIVATE(Foo) Q_DECLARE_PRIVATE(FooPrivate) ... ... }; }; 
  • El puntero PIMPL debe ser const para clases no copiables / no asignables como QObject . Puede ser no const al implementar clases copiables.

  • Como el PIMPL es un detalle de implementación interna, su tamaño no está disponible en el sitio donde se utiliza la interfaz. La tentación de utilizar la colocación nueva y la fraseología Fast Pimpl debe ser resistida ya que no proporciona beneficios para nada más que una clase que no asigna memoria en absoluto.

La implementación

El PIMPL debe definirse en el archivo de implementación. Si es grande, también se puede definir en un encabezado privado, habitualmente llamado foo_p.h para una clase cuya interfaz está en foo.h

El PIMPL, como mínimo, es meramente un portador de los datos de la clase principal. Solo necesita un constructor y ningún otro método. En nuestro caso, también necesita almacenar el puntero a la clase principal, ya que querremos emitir una señal desde la clase principal. Así:

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

El PIMPL no se puede copiar. Como usamos miembros que no pueden copiarse, cualquier bash de copiar o asignar al PIMPL sería capturado por el comstackdor. En general, es mejor deshabilitar explícitamente la funcionalidad de copia mediante Q_DISABLE_COPY .

La macro Q_DECLARE_PUBLIC funciona de manera similar a Q_DECLARE_PRIVATE . Se describe más adelante en esta sección.

Pasamos el puntero al diálogo en el constructor, lo que nos permite inicializar el diseño en el diálogo. También conectamos la señal aceptada de onAccepted ranura interna en 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 } 

El método PIMPL onAccepted() debe exponerse como una ranura en proyectos Qt 4 / non-C ++ 11. Para Qt 5 y C ++ 11, esto ya no es necesario.

Al aceptar el diálogo, capturamos las coordenadas y emitimos la señal de Coordenadas acceptedCoordinates . Es por eso que necesitamos el puntero público:

 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 declara una variable CoordinateDialog * const q local. Se describe más adelante en esta sección.

La parte pública de la implementación construye el PIMPL y expone sus propiedades:

 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 declara una variable CoordinateDialogPrivate * const d local. Se describe a continuación.

La macro Q_D

Para acceder al PIMPL en un método de interfaz , podemos usar la macro Q_D , pasándole el nombre de la clase de interfaz.

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

Para acceder al PIMPL en un método de interfaz const , debemos anteponer el nombre de la clase con la palabra clave const :

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

La macro Q_Q

Para acceder a la instancia de interfaz desde un método PIMPL no const , podemos usar la macro Q_Q , pasándole el nombre de la clase de interfaz.

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

Para acceder a la instancia de la interfaz en un método const PIMPL , anteponemos el nombre de la clase con la palabra clave const , tal como lo hicimos para 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

Esta macro es opcional y se usa para permitir el acceso a la interfaz desde el PIMPL. Normalmente se usa si los métodos del PIMPL necesitan manipular la clase base de la interfaz o emitir sus señales. Se Q_DECLARE_PRIVATE macro Q_DECLARE_PRIVATE equivalente para permitir el acceso al PIMPL desde la interfaz.

La macro toma el nombre de la clase de interfaz como parámetro. Declara dos implementaciones en línea del método de ayuda q_func() . Ese método devuelve el puntero de la interfaz con la constness adecuada. Cuando se usa en métodos const, devuelve un puntero a una interfaz const . En métodos no const, devuelve un puntero a una interfaz no const. También proporciona la interfaz del tipo correcto en las clases derivadas. Se deduce que todo el acceso a la interfaz desde dentro del PIMPL se debe hacer usando q_func() y ** no a través de q_ptr . Usualmente usamos la macro Q_Q , descrita arriba.

La macro espera que el puntero a la interfaz se denomine q_ptr . No hay una variante de dos argumentos de esta macro que permita elegir un nombre diferente para el puntero de la interfaz (como fue el caso de Q_DECLARE_PRIVATE ).

La macro se expande de la siguiente manera:

 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

Esta macro elimina el constructor de copia y el operador de asignación. Debe aparecer en la sección privada del PIMPL.

Gotchas comunes

  • El encabezado de interfaz para una clase determinada debe ser el primer encabezado que se debe incluir en el archivo de implementación. Esto obliga a que el encabezado sea independiente y no dependa de las declaraciones que se incluyan en la implementación. Si no es así, la implementación no podrá comstackrse, lo que le permitirá corregir la interfaz para que sea autosuficiente.

     // 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 debe aparecer en la sección privada de PIMPL

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

Clases PIMPL y no cobrables con QObject

La expresión idiomática PIMPL le permite a uno implementar objetos asignables que pueden copiarse, copiarse y moverse mediante movimiento. La asignación se realiza mediante el modismo copiar y cambiar , evitando la duplicación de código. El puntero PIMPL no debe ser const, por supuesto.

Recuerde el en C ++ 11, necesitamos prestar atención a la Regla de los Cuatro y proporcionar todo lo siguiente: el constructor de copia, el constructor de movimiento, el operador de asignación y el destructor. Y la función de swap independiente para implementarlo todo, por supuesto †.

Ilustraremos esto usando un ejemplo bastante inútil, pero sin embargo correcto.

Interfaz

 // 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 el rendimiento, el constructor de movimiento y el operador de asignación se deben definir en el archivo de interfaz (encabezado). No necesitan acceder al PIMPL directamente:

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

Todos usan la función independiente de swap , que también debemos definir en la interfaz. Tenga en cuenta que es

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

Implementación

Esto es bastante sencillo. No necesitamos acceso a la interfaz desde el PIMPL, por lo tanto Q_DECLARE_PUBLIC y q_ptr están 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 esta excelente respuesta : hay otras afirmaciones de que debemos especializar std::swap para nuestro tipo, proporcionar un swap en clase junto con un swap libre de funciones, etc. Pero todo esto es innecesario: cualquier uso adecuado de swap será a través de una llamada no calificada, y nuestra función se encontrará a través de ADL . Una función servirá.