D-Pointer/es
Español 简体中文 Български English
¿Qué es el d-pointer?
Si has leído el ficheros fuente de Qt, como éste [qt.gitorious.org], 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 [es.wikipedia.org]) 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:
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.
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”:https://es.wikipedia.org/wiki/Función_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).
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:
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.
¿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.
Por tanto, cuando se accede al d-pointer en una clase derivada, necesitamos un static_cast con el tipo apropiado.
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
label.cpp
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
Esta macro se puede puede usar de esta manera:
qlabel.h
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. También hay una
Q_DECLARE_PUBLIC
que hace lo contrario.