D-Pointer/es

From Qt Wiki
Jump to navigation Jump to search
This article may require cleanup to meet the Qt Wiki's quality standards. Reason: Auto-imported from ExpressionEngine.
Please improve this article if you can. Remove the {{cleanup}} tag and add this page to Updated pages list after it's clean.

Español 简体中文 Български English

¿Qué es el d-pointer?

Si has leído el ficheros fuente de Qt, como éste, habrás encontrado por todas partes las macros

Q_D

y

Q_Q

macros. Este artículo desentraña el propósito de dichas macros. Las macros

Q_D

y

Q_Q

son parte de un patrón de diseño denominado "d-pointer" (también llamado puntero opaco) mediante el cual los detalles de implementación de una librería se pueden ocultar a sus usuarios y se pueden realizar cambios en la implementación sin afectar a la compatibilidad binaria.

Compatibilidad binaria, ¿qué es eso?

Cuando se diseñan librerias como Qt, es deseable que las aplicaciones que enlaza con Qt dinámicamente continúen funcionando sin recompilar incluso después de que la librería Qt sea actualizada o reemplazada por otra versión. Por ejemplo, si tu aplicación CuteApp se basa en Qt 4.5, deberías ser capaz de actualizar las librerías Qt (en Windows son proporcionan con la aplicación, ¡en Linux a menudo vienen automáticamente mediante el administrador de paquetes!) de la versión 4.5 a Qt 4.6 y tu CuteApp, que fue compilada con Qt 4.5, debería poder seguir funcionando.

¿Qué afecta a la compatibilidad binaria?

Entonces, ¿en qué caso provoca un cambio en la librería la recompilación de la aplicación? Vamos a ver un ejemplo sencillo:

 class Widget {
 
 private:
 Rect m_geometry;
 };

class Label : public Widget {
 
 String text() const { return m_text; }
 private:
 String m_text;
 };

Aquí tenemos un Widget que contiene como variable su geometría. Compilamos nuestro Widget y lo publicamos como WidgetLib 1.0

Para WidgetLib 1.1, alguien viene con la brillante idea de agregar soporte para hojas de estilo. Sin problmas, simplemente añadimos unos métodos nuevos y agregamos un atributo.

 class Widget {
 
 private:
 Rect m_geometry;
 String m_stylesheet; // NUEVO en WidgetLib 1.1
 };

class Label : public Widget {
 public:
 
 String text() const { return m_text; }
 private:
 String m_text;
 };

Publicamos WidgetLib 1.1 con los cambios anteriores para encontrarnos con que CuteApp, que fue compilada con WidgetLib 1.0 y funcionaba perfectamente, ¡se estrella estrepitosamente!

¿Por qué se rompió?

La razón es que, al agregar un nuevo atributo, hemos cambiado el también el tamaño de los objetos Widget y Label. ¿Y qué importa eso? Cuando el compilador C++ genera código, usa 'offsets' (desplazamientos) para acceder a los datos que hay dentro de un objeto.

Aquí tenemos una versión muy simplificada de cómo aparecerían dichos objetos en la memoria.

Disposición del objeto Label en WidgetLib 1.0 Disposición del objeto label en WidgetLib 1.1
m_geometry <offset 0> m_geometry <offset 0>
————— m_stylesheet <offset 1>
m_text <offset 1> ——————  
————— m_text <offset 2>

En WidgetLib 1.0, el miembro text de Label se encontraba en el desplazamient (lógico) 1. El código que el compilador ha generado en la aplicación para el método

Label::text()

se traduce a un acceso al desplazamiento 1 del objeto "label" en la aplicación. En WidgetLib 1.1, ¡el miembro "text" de la Label se ha movido al desplazamiento (lógico) 2! Dado que no hemos recompilado la aplicación, ésta sigue pensando que

text

se encuentra en el desplazamiento 1 ¡y acaba accediendo a la variable

stylesheet

! Estoy seguro de que en este momento hay unos cuántos preguntándose por qué el cálculo del desplazamiento de

Label::text()

acabó en el binario de CuteApp y no en el de WidgetLib. La respuesta es que el código de

Label::text()

se definió en el fichero de cabecera y el compilador terminó por insertarlo (inline). Por tanto, ¿cambiaría la situación si

Label::text()

no hubiera sido convertida en "inline"? Digamos, en el caso de haber movido

Label::text()

al fichero fuente? Pues no. El compilador C++ se basa en que el tamaño de los objetos va a ser el mismo al compilar y al ejecutar. Por ejemplo, para el desenrollado de pila (si has creado un objeto Label en la pila, el compilador habrá generado código para crear espacio en la pila basándose en el tamaño de Label a la hora de compilar) . Dado que el tamaño de Label es diferente al ejecutarse con WidgetLib 1.1, el constructor de Label va a sobreescribir datos ya existentes en la pila y terminará corrompiéndola.

Nunca cambies el tamaño de una clase de C++ exportada

En resumen, nunca jamás cambies el tamaño o distribución (no alteres la posición de los datos) de clases de C++ exportadas (es decir, visibles para el usuario) una vez que hayas publicado tu librería. El compilador C++ genera código asumiendo que el tamaño y orden de los datos en una clase no cambiará después de que la aplicación ha sido compilada.

Entonces, ¿cómo puede uno agregar nuevas propiedades sin alterar el tamaño de los objetos?

El d-pointer

El truco está en mantener constante el tamaño de todas las clases públicas de una librería, almacenando un único puntero. Este puntero apunta a una estructura de dato privada/interna que contiene todos los datos. El tamaño de esta estructura interna puede encoger o crecer sin tener ningún efecto secundario sobre la aplicación porque a este puntero se acceder sólo desde el código de la librería y, desde el punto de vista de la aplicación, el tamaño del objeto nunca cambia (siempr es del tamaño de un puntero). A este puntero lo llamamos el d-pointer.

El espíritu de este patrón lo esbozamos en el siguiente código (el código en este artículo no contiene destructures, pero por supuesto deberíamos añadirlos en código real).

 /* widget.h */
 // Declaración anticipada. La definición estará en widget.cpp o
 // en un fichero aparte, digamos widget_p.h
 class WidgetPrivate;

class Widget {
 
 Rect geometry() const;
 
 private:
 // nunca se hace referencia al d-pointer en el fichero de cabecera.
 // dado que WidgetPrivate no está definida en esta cabecera,
 // cualquier acceso provocará un error de compilación
 WidgetPrivate '''d_ptr;
 };

 /''' widget_p.h */ (_p significa privado)
 struct WidgetPrivate {
 Rect geometry;
 String stylesheet;
 };

 /''' widget.cpp */
 #include "widget_p.h"
 Widget::Widget()
 : d_ptr(new WidgetPrivate) {// creamos los datos privados
 }

 Rect Widget::geoemtry() const {
 // sólo se accede al d-ptr en el código de la librería
 return d_ptr->geometry;
 }

 /''' label.h */
 class Label : public Widget {
 
 String text();
 private:
 // cada clase mantiene su propio d-pointer
 LabelPrivate '''d_ptr;
 };

 /''' label.cpp */
 // Al contrario que en WidgetPrivate, definimos LabelPrivate en el propio fichero fuente
 struct LabelPrivate {
 String text;
 };

 Label::Label()
 : d_ptr(new LabelPrivate) {
 }

 String Label::text() {
 return d_ptr->text;
 }

Con la estructura anterior, CuteApp nunca accede directamente al d-pointer. Y dado que los únicos accessos al d-pointer están en WidgetLib, que se recompila con cada nueva publicación, se puede cambiar con libertad la clase Private sin tener impacto en CuteApp.

Otros beneficios del d-pointer

La compatibilidad binaria no lo es todo. El d-pointer tiene otras ventajas: Oculta detalles de implementación - Podemos publicar simplemente los ficheros de cabecera y binarios de WidgetLib. Los ficheros .cpp pueden ser fuente cerrada.

  • El fichero de cabecera está limpio de detalles de implementación y puede servir como API de referencia.
  • Dado que los ficheros de cabecera necesarios para la implementación se han pasado desde un fichero de cabecera al fichero (fuente) de implementación, las compilaciones serán mucho más rápidas.

Ciertamente, estas ventajas pueden parecer triviales. La razón real para el uso de los d-pointer en Qt es la compatibilidad binaria y el hecho de que Qt comenzó siendo de código cerrado.

El q-pointer

Hasta ahora, sólo hemos visto el d-pointer como una estructura de datos de tipo C. En realidad, contiene métodos privados (funciones de ayuda). Por ejemplo,

LabelPrivate

podría tener una función de ayuda

getLinkTargetFromPoint()

que se precisa para encontrar el objetivo cuando se pulsa el ratón. En muchos casos, estos métodos de ayuda requieren de acceso a la clase pública (es decir, algunas funciones de Label o de su clase base, Widget). Por ejemplo,

setTextAndUpdateWidget()

(un método de ayuda), podría querer llamar a

Widget::update()

, que es un método público para solicitar una repintada de Widget. Por tanto, el

WidgetPrivate

almacena un puntero a la clase pública llamado el q-pointer. Modificando el código de arriba para tener el q-pointer obtenemos:

 /* widget.h */
 // Declaración anticipada. La definición estará en widget.cpp o
 // en un fichero aparte, digamos widget_p.h
 class WidgetPrivate;

class Widget {
 
 Rect geometry() const;
 
 private:
 // nunca se hace referencia al d-pointer en el fichero de cabecera.
 // dado que WidgetPrivate no está definida en esta cabecera,
 // cualquier acceso provocará un error de compilación
 WidgetPrivate '''d_ptr;
 };

 /''' widget_p.h */ (_p significa privado)
 struct WidgetPrivate {
 // constructor que inicializa el q-ptr
 WidgetPrivate(Widget *q) : q_ptr(q) { }
 Widget '''q_ptr; // q-ptr que apunta a la clase de API
 Rect geometry;
 String stylesheet;
 };

 /''' widget.cpp */
 #include "widget_p.h"
 // creamos los datos privados. pasamos el puntero 'this' para inicializar el q-ptr
 Widget::Widget()
 : d_ptr(new WidgetPrivate(this)) {
 }

 Rect Widget::geometry() const {
 // sólo se accede al d-ptr en el código de la librería
 return d_ptr->geometry;
 }

 /''' label.h */
 class Label : public Widget {
 
 String text() const;
 private:
 LabelPrivate '''d_ptr; // cada clase mantiene su propio d-pointer
 };

 /''' label.cpp */
 // Al contrario que en WidgetPrivate, definimos LabelPrivate en el propio fichero fuente
 struct LabelPrivate {
 LabelPrivate(Label *q) : q_ptr(q) { }
 Label '''q_ptr;
 String text;
 };

 Label::Label()
 : d_ptr(new LabelPrivate(this)) {
 }

 String Label::text() {
 return d_ptr->text;
 }

Herencia de d-pointers para optimización

En el código anterior, la creación de una Label resulta en la reserva de memoria para

LabelPrivate

y

WidgetPrivate

. Si empleásemos esta estrategia para Qt, la situación se volvería mucho peor en clases como

QListWidget

, que se encuentra a 6 niveles en la jerarquía de herencia de clases ¡y resultaría en hasta 6 reservas de memoria!

Eto se resuelve teniendo una jerarquía de herencia para nuestras clases privadas y haciendo que la clase que se está instanciando pase el d-pointer hacia arriba.

Observa que cuando se heredan d-pointers, la declarción de la clase privada tiene que estar en un fichero separado, por ejemplo widget_p.h. No se puede seguir declarando en el fichero widget.cpp.

 /''' widget.h */
 class Widget {
 public:
 Widget();
 
 protected:
 // sólo las subclases pueden acceder a lo siguiente
 Widget(WidgetPrivate &d); // permitir a las subclases inicializarse usando su propio Private concreto
 WidgetPrivate '''d_ptr;
 };

 /''' widget_p.h */ (_p significa private)
 struct WidgetPrivate {
 WidgetPrivate(Widget *q) : q_ptr(q) { } // constructor que inicializa el q-ptr
 Widget '''q_ptr; // q-ptr que apunta a la clase de API
 Rect geometry;
 String stylesheet;
 };

 /''' widget.cpp */
 Widget::Widget()
 : d_ptr(new WidgetPrivate(this)) {
 }

 Widget::Widget(WidgetPrivate &d)
 : d_ptr(&d) {
 }

 /''' label.h */
 class Label : public Widget {
 public:
 Label();
 
 protected:
 Label(LabelPrivate &d); // allow Label subclasses to pass on their Private
 // observa cómo Label ¡no tiene un d_ptr! Simplemente usa el de Widget
 };

 /''' label.cpp */
 #include "widget_p.h" // de manera que podamos acceder a WidgetPrivate

class LabelPrivate : public WidgetPrivate {
 public:
 String text;
 };

Label::Label()
 : Widget(*new LabelPrivate) // inicializar el d-pointer con nuestro propio Private
 }

Label::Label(LabelPrivate &d)
 : Widget(d) {
 }

¿Ves la belleza? Ahora cuando creamos un objeto

Label

, creará un

LabelPrivate

(que deriva de

WidgetPrivate

). ¡Pasa el d-pointer en concreto al constructor protegido de Widget! Ahora, cuando se crea un objeto

Label

, sólo se hace una reserva de memoria. Label tiene también un constructor protegido que pueden usar sus clases derivadas para proporcionar sus propias clases privadas.

d-pointers en Qt

En Qt, prácticamente cada clase pública usa el enfoque d-pointer. Los únicos casos en los que no se usa es cuando se sabe con anticipación que nunca se van a agregar miembros nuevos a la clase. Por ejemplo, no se espera añadir miembros nuevos para clases como

QPoint

o

QRect

y, por tanto, los miembros datos se almacenan directmente en la clase en lugar de usar el d-pointer. Observa que, en Qt, la clase base de todos los objetos Private es

QObjectPrivate

.

Q_D and Q_Q

Un efecto colateral a la optimización que hicimos en el paso anterior es que el q-ptr y el d-ptr son de tipo

Widget

y

WidgetPrivate

. Eso significa que lo siguiente no va a funcionar.

 void Label::setText(const String &text) {
 // ¡no funcionará! ya que d_ptr es de tipo WidgetPrivate incluso aunque apunta a un objeto LabelPrivate
 d_ptr->text = text;
 }

Por tanto, cuando se accede al d-pointer en una clase derivada, necesitamos un static_cast con el tipo apropiado.

 void Label::setText(const String &text) {
 LabelPrivate '''d = static_cast<LabelPrivate'''>(d_ptr); // convertir a nuestro tipo privado
 d->text = text;
 }

Como puedes ver, no es bonito tener static_cast por todos lados. En su lugar, se han definido dos macros en src/corelib/global/qglobal.h, que lo convierte en algo más sencillo:

global.h

 #define Q_D(Class) Class##Private * const d = d_func()
 #define Q_Q(Class) Class * const q = q_func()

label.cpp

//Con Q_D se pueden usar miembros de LabelPrivate desde Label
 void Label::setText(const String &text) {
 Q_D(Label);
 d->text = text;
 }
//Con Q_Q puedes usar los miembros de Label desde LabelPrivate
 void LabelPrivate::someHelperFunction() {
 Q_Q(Label);
 q->selectAll();
 }

Q_DECLARE_PRIVATE y Q_DECLARE_PUBLIC

Las clases de Qt tienen una macro

Q_DECLARE_PRIVATE

en la clase pública. La macro es como sigue:

qglobal.h

 #define Q_DECLARE_PRIVATE(Class)  inline Class##Private* d_func() { return reinterpret_cast<Class##Private '''>(qGetPtrHelper(d_ptr)); }  inline const Class##Private''' d_func() const {  return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr)); }  friend class Class##Private;

Esta macro se puede puede usar de esta manera:

qlabel.h

 class QLabel {
 private:
 Q_DECLARE_PRIVATE(QLabel);
 };

La idea es que

QLabel

proporcione una función

d_func()

que permita el acceso a su clase privada interna. Este método en sí es privado (dado que la macro está en una sección privada de qlabel.h). Sin embargo,

d_func()

puede ser invocada por clases amigas (C++ friend) de

QLabel

. Esto es útil principalmente para que clases de Qt que no pueden acceder a la API pública de

QLabel

puedan acceder a la información. Un ejemplo estrafalario de esto sería que

QLabel

quiera llevar un conteo del número de veces que un usuario ha pulsado en un enlace. Sin embargo, no hay API pública para acceder a esta información.

QStatistics

es una clase que necesita esta información. Un desarrollador de Qt añadirá

QStatistics

como clase amiga de

QLabel

y

QStatistics

podrá entonces hacer

label->d_func()->linkClickCount

. La

d_func

tiene también la ventaja de forzar la corrección de const: en una función miembro const de MyClass, necesitarás una Q_D(const MyClass) y, por tanto, sólo podrás invocar a funciones miembro const de MyClassPrivate. Con un d_ptr "desnudo" también podrías invocar funciones que no sean const.