D-Pointer/es: Difference between revisions

From Qt Wiki
Jump to navigation Jump to search
No edit summary
 
No edit summary
Line 1: Line 1:
'''Español''' [[Dpointer-SimplifiedChinese|简体中文]] [[Dpointer Bulgarian|Български]] [[Dpointer|English]]


=¿Qué es el d-pointer?=
Si has leído el ficheros fuente de Qt, como [http://qt.gitorious.org/qt/qt/blobs/HEAD/src/gui/widgets/qlabel.cpp éste] ''[qt.gitorious.org]'', habrás encontrado por todas partes las macros <code>Q_D</code> y <code>Q_Q</code> macros. Este artículo desentraña el propósito de dichas macros.
Las macros <code>Q_D</code> y <code>Q_Q</code> son parte de un patrón de diseño denominado “d-pointer” (también llamado [http://es.wikipedia.org/wiki/Puntero_opaco 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''.<br />
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.
{| class="infotable line"
| '''Disposición del objeto Label en WidgetLib 1.0'''
| '''Disposición del objeto label en WidgetLib 1.1'''
|-
| m_geometry &lt;offset 0&gt;
| m_geometry &lt;offset 0&gt;
|-
| ———————
| m_stylesheet &lt;offset 1&gt;
|-
| m_text &lt;offset 1&gt;
| ————————-
|-
| ———————-
| m_text &lt;offset 2&gt;
|}
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 <code>Label::text()</code> 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 <code>text</code> se encuentra en el desplazamiento 1 ¡y acaba accediendo a la variable <code>stylesheet</code>!
Estoy seguro de que en este momento hay unos cuántos preguntándose por qué el cálculo del desplazamiento de <code>Label::text()</code> acabó en el binario de CuteApp y no en el de WidgetLib. La respuesta es que el código de <code>Label::text()</code> se definió en el fichero de cabecera y el compilador terminó por insertarlo (“inline”:[https://es.wikipedia.org/wiki/Función_inline). https://es.wikipedia.org/wiki/Función_inline).]
Por tanto, ¿cambiaría la situación si <code>Label::text()</code> no hubiera sido convertida en “inline”? Digamos, en el caso de haber movido <code>Label::text()</code> 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 <span class="caps">API</span> 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, <code>LabelPrivate</code> podría tener una función de ayuda <code>getLinkTargetFromPoint()</code> 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, <code>setTextAndUpdateWidget()</code> (un método de ayuda), podría querer llamar a <code>Widget::update()</code>, que es un método público para solicitar una repintada de Widget. Por tanto, el <code>WidgetPrivate</code> 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 <code>LabelPrivate</code> y <code>WidgetPrivate</code>. Si empleásemos esta estrategia para Qt, la situación se volvería mucho peor en clases como <code>QListWidget</code>, 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 <code>Label</code>, creará un <code>LabelPrivate</code> (que deriva de <code>WidgetPrivate</code>). ¡Pasa el ''d-pointer'' en concreto al constructor protegido de Widget! Ahora, cuando se crea un objeto <code>Label</code>, 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 <code>QPoint</code> o <code>QRect</code> 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 <code>QObjectPrivate</code>.
===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 <code>Widget</code> y <code>WidgetPrivate</code>. 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.<br />
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'''<br />
'''label.cpp'''<br />
===Q_DECLARE_PRIVATE y Q_DECLARE_PUBLIC===
Las clases de Qt tienen una macro <code>Q_DECLARE_PRIVATE</code> en la clase pública. La macro es como sigue:
'''qglobal.h'''<br />
Esta macro se puede puede usar de esta manera:
'''qlabel.h'''<br />
La idea es que <code>QLabel</code> proporcione una función <code>d_func()</code> 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, <code>d_func()</code> puede ser invocada por clases '''amigas''' (C++ friend) de <code>QLabel</code>. Esto es útil principalmente para que clases de Qt que no pueden acceder a la <span class="caps">API</span> pública de <code>QLabel</code> puedan acceder a la información. Un ejemplo estrafalario de esto sería que <code>QLabel</code> quiera llevar un conteo del número de veces que un usuario ha pulsado en un enlace. Sin embargo, no hay <span class="caps">API</span> pública para acceder a esta información. <code>QStatistics</code> es una clase que necesita esta información. Un desarrollador de Qt añadirá <code>QStatistics</code> como clase amiga de <code>QLabel</code> y <code>QStatistics</code> podrá entonces hacer <code>label-&gt;d_func()-&gt;linkClickCount</code>.
La <code>d_func</code> 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 <code>Q_DECLARE_PUBLIC</code> que hace lo contrario.
===Categories:===
* [[:Category:Developing-with-Qt|Developing with Qt]]
* [[:Category:HowTo|HowTo]]
* [[:Category:QtDevelopmentSpanish|QtDevelopmentSpanish]]
** [[:Category:QtDevelopmentSpanish::General|General]]
* [[:Category:QtInternals|QtInternals]]
* [[:Category:Spanish|Spanish]]

Revision as of 07:44, 24 February 2015