Español 简体中文 Български English
Table of Content
Que es el d-pointer?
Si alguna vez has mirado las fuentes de Qt como ésta [qt.gitorious.com], usted vera generosamente salpicada con Q_D y Q_Q. Este articulo revela trata el proposito de estos macros. Los macros Q_D y Q_Q son parte de un patron de diseño llamado d-pointer (tambien llamado opaque pointer) donde los detalles de la implementacion de una libreria pueden ser ocultados de sus usuarios y los cambios en la implementacion pueden ser hechos a la libreria sin romper la compatibilidad binaria.
Compatibilidad binaria, qué es eso?
Cuando se diseñan librerias como Qt, se desea que las aplicaciones que estan dinamicamente conectadas con Qt continuen funcionando sin recompilar incluso despues de que la libreria Qt sea actualizada/reemplazada con otra version. Por ejemplo, si tu aplicacion CoolApp fuera basada en Qt 4.5, usted deberia ser capaz de actualizar las librerias Qt (en Windows son enviadas con la aplicacion, en Linux a menudo vienen del administrador de paquetes automaticamente!) de la version 4.5 a Qt 4.6 y tu CoolApp que fue creada con Qt 4.5 aun deberia ser capaz de funcionar.
Que rompe la compatibilidad binaria?
Entonces, cuando un cambio en la libreria requiere la recompilacion de la aplicacion? Vamos a tomar un ejemplo simple:
- class Widget {
- ...
- private:
- Rect m_geometry;
- };
- class Label : public Widget {
- ...
- String text() const { return m_text; }
- private:
- String m_text;
- };
Aqui tenemos un Widget que contiene geometria como una variable. Compilamos nuestro Widget y lo enviamos como WidgetLib 1.0
Para WidgetLib 1.1, alguien viene con la brillante idea de agregar soporta para stylesheets. Sin dudar, nosotros solo agregamos unos nuevos metodos y agregamos un nuevo atributo.
- class Widget {
- ...
- private:
- Rect m_geometry;
- String m_stylesheet; // NEW in WidgetLib 1.1
- };
- class Label : public Widget {
- public:
- ...
- String text() const { return m_text; }
- private:
- String m_text;
- };
Enviamos WidgetLib 1.1 con los cambios solo para ver que CoolApp que fue compilada con y funcionaba con WidgetLib 1.0 cae gloriosamente!
Por que cayó?
La razon es que agregando un nuevo atributo, terminamos cambiando el tamaño de los objetos Widget y Label. Por que esto importa? Cuando tu compilador de C++ genera codigo, el utiliza (rangos)‘offsets’ para acceder a los datos dentro de un objeto.
Aqui tenemos una version sobre-siplificada de como los objetos mencionados podrian lucir en la memoria.
| Label object layout in WidgetLib 1.0 | Label object layout in 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 era en la posicion (logica) offset 1. El codigo generado por el compilador en la aplicacion para el metodo era Label::text() traduce el acceso offset 1 del objeto label en la aplicacion. En WidgetLib 1.1, el miembro “text” del Label ha cambiado a la posicion logica offset 2!. Desde que la aplicacion no ha sido recompilada, la misma continua pensando que “text” se encuentra en la posicion offset 1 y termina accesando la variable stylesheet!
Para aquellos que han completado C++ 101:-)
Estoy seguro que en este punto algunos estan deseando saber porque el codigo calculado de la posicion de Label::text () termino en el binario de CoolApp y no en el binario de WidgetLib. La respuesta es que el codigo para Label:text() fue definido en el header file y el compilador termino haciendo el inlining con el [en.wikipedia.org]. Entonces, Cambiaria la situacion si Label::text() no hubiera sido inlined? Digamos, Label::text() fue movida al source file? bueno, no. El compilador C++ se apoya en el tamaño de los objetos siendo el mismo en tiempo de compilacion y ejecucion. Por ejemplo, stack winding/unwinding – si usted crea un objeto Label en el stack, el compliador genera codigo para suficiente espacio en el stack basado en el tamaño de Label en tiempo de compilacion. Desde que el tamaño de Label es diferente en tiempo de ejecucion en WidgetLib 1.1, el constructor de Label sobre-escribe datos existentes en el stack y termina corrompiendo el stack.
Nunca cambies el tamaño de una clase de C++ exportada
En resumen, nunca jamas cambies el tamaño o distribucion (no muevas las posiciones de los datos) de clases de C++ exportadas (ej. visibles para el usuario) una vez que tu libreria ha sido resuelta. El compilador C++ genera codigo asumiendo que el tamaño y orden de los datos en una clase no cambiara despues de que la aplicacion ha sido compilada. Entonces, Como puede uno agregar nuevas propiedades sin alterar el tamaño de los objetos?
El d-pointer
El truco es conservar el tamaño de todas las clases publicas de la libreria constantes solo con agregar un unico puntero. Este puntero apunta a la estructura de datos privada/interna que contiene todos los datos. El tamaño de esta estructura interna puede disminuir o aumentar sin tener ningun efecto adverso en la aplicacion porque el puntero es accesado solo en el codigo de la libreria y desde el punto de vista de la aplicacion el tamaño del objeto nunca cambia – es siempre el tamaño del puntero. Este puntero es llamado el d-pointer.
El espiritu de este patron esta descrito en el siguiente codigo:
- /* widget.h */
- // Forward-declare. The definition will be in widget.cpp or
- // in a separate file, say widget_p.h
- class WidgetPrivate;
- class Widget {
- ...
- Rect geometry() const;
- ...
- private:
- // d-pointer never referenced in header file.
- // Since WidgetPrivate is not defined in this header,
- // any access will be a compile error
- WidgetPrivate *d_ptr;
- };
- /* widget_p.h */ (_p means private)
- struct WidgetPrivate {
- Rect geometry;
- String stylesheet;
- };
- /* widget.cpp */
- #include "widget_p.h"
- Widget::Widget()
- : d_ptr(new WidgetPrivate) {// create private data
- }
- Rect Widget::geoemtry() const {
- // the d-ptr is only accessed in the library code
- return d_ptr->geometry;
- }
- /* label.h */
- class Label : public Widget {
- ...
- String text();
- private:
- // each class maitains it's own d-pointer
- LabelPrivate *d_ptr;
- };
- /* label.cpp */
- // Unlike WidgetPrivate, we define LabelPrivate in the source file itself
- struct LabelPrivate {
- String text;
- };
- Label::Label()
- : d_ptr(new LabelPrivate) {
- }
- String Label::text() {
- return d_ptr->text;
- }
Con la estructura superior, CoolApp nunca accesa el d-pointer directamente. Y desde qe el d-pointer es solamente accesado en WidgetLib y WidgetLib es recompilada para cada release, la clase privada puede cambiar libremente sin tener un impacto en CoolApp.
Otros beneficios del d-pointer
No es todo acerca de compatibilidad binaria. El d-pointer tambien tiene otros beneficios:
- Ocultar detalles de implementacion – Nosotros podemos enviar solamente WidgetLib solo con los header files y los binarios. los .cpp pueden ser de codigo cerrado.
- El header file permanece limpio de detalles de implementacion y sirve como API reference.
- Desde que los header files necesarios para la implementacion son movidos del header file dentro de la implementacion (source file), la compilacion es mucho mas rapida.
Tambien es cierto que los benefecios de arriba aparentan triviales. La verdadera razon para usar d-pointers en Qt es por compatibilidad binaria y que el de que Qt comenzo como codigo cerrado.
El q-pointer
Hasta ahora, somo hemos visto el d-pointer como un estructura de datos de tipo C-style. En realidad, contiene los metotos privados (funciones helper). Por ejemplo, LabelPrivate puede tener una funcion helper getLinkTargetFromPoint() que es requerida para encontrar la conexion cuando el mouse es presionado. En muchos casos, estas funciones helper requieren acceso a la clase publica ej. algunas funciones de Label o de su clase base Widget. Por ejemplo, un metodo helper, setTextAndUpdateWidge() puede necesitar llamar Widget::update() el cual es un metodo publico que agenda una repintada del Widget. Entonces, el WidgetPrivate posee un puntero a la clase publica llamado q-pointer. Modificando el codigo superior para el q-pointer tenemos:
- /* widget.h */
- // Forward-declare. The definition will be in widget.cpp
- // or in a separate file, say widget_p.h
- class WidgetPrivate;
- class Widget {
- ...
- Rect geometry() const;
- ...
- private:
- // d-pointer never referenced in header file.
- // Since WidgetPrivate is not defined in this header,
- // any access will be a compile error
- WidgetPrivate *d_ptr;
- };
- /* widget_p.h */ (_p means private)
- struct WidgetPrivate {
- // constructor that initializes the q-ptr
- WidgetPrivate(Widget *q) : q_ptr(q) { }
- Widget *q_ptr; // q-ptr that points to the API class
- Rect geometry;
- String stylesheet;
- };
- /* widget.cpp */
- #include "widget_p.h"
- // create private data. pass the 'this' pointer to initialize the q-ptr
- Widget::Widget()
- : d_ptr(new WidgetPrivate(this)) {
- }
- Rect Widget::geoemtry() const {
- // the d-ptr is only accessed in the library code
- return d_ptr->geometry;
- }
- /* label.h */
- class Label : public Widget {
- ...
- String text() const;
- private:
- LabelPrivate *d_ptr; // each class maitains it's own d-pointer
- };
- /* label.cpp */
- // Unlike WidgetPrivate, we define LabelPrivate in the source file
- 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;
- }
Mas optimizacion
En el codigo superior, crear un Label resulta en usar espacio de memoria para LabelPrivate y WidgetPrivate. Si nosotros fueramos a emplear esta estrategia para Qt, la situacion empeora para clases como QListWidget – que esta 6 niveles dentro de la jerarquia de herencia de las clases y resultaria en mas de 6 espacios de memoria!
Esto es resuelto teniendo una jerarquia de herencia para nuestras clases privadas y teniendo la clase instanciando en el d-pointer todo el camino hacia arriba.
- /* widget.h */
- class Widget {
- public:
- Widget();
- ...
- protected:
- // only sublasses may access the below
- Widget(WidgetPrivate &d); // allow subclasses to initialize with their own concrete Private
- WidgetPrivate *d_ptr;
- };
- /* widget_p.h */ (_p means private)
- struct WidgetPrivate {
- WidgetPrivate(Widget *q) : q_ptr(q) { } // constructor that initializes the q-ptr
- Widget *q_ptr; // q-ptr that points to the API class
- 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
- // notice how Label does not have a d_ptr! It just uses Widget's d_ptr.
- };
- /* label.cpp */
- #include "widget_p.h" // so we can access WidgetPrivate
- class LabelPrivate : public WidgetPrivate {
- public:
- String text;
- };
- Label::Label()
- : Widget(*new LabelPrivate) // initialize the d-pointer with our own Private {
- }
- Label::Label(LabelPrivate &d)
- : Widget(d) {
- }
Puedes ver la belleza? Cuando nosotros ahora creamos un objeto Label , el creara un LabelPrivate (el cual es una sub clase de WidgetPrivate). El pasa el d-pointer contreto al constructor protected de Widget! Ahora, cuando un objeto label es creado, hay solo una memoria guardada. Label tambien tiene un constructor protected que sera usado por la clase que hereda Label para proveer su propio Private.
Haciendo casting de q-ptr y d-ptr al tipo de datos correcto (QPTR y DTPR)
El efecto colateral de la optimizacion que hicimos en el paso anterior es que el q-ptr y d-ptr son del tipo Widget y WidgetPrivate. Esto significa que lo siguiente no funcionara:
- void Label::setText(const String &text) {
- // won't work! since d_ptr is of type WidgetPrivate even though it points to LabelPrivate object
- d_ptr->text = text;
- }
Por tanto, cuando accesamos el d-pointer en una sub clase, necesitamos hacer un static_cast al tipo apropiado.
- void Label::setText(const String &text) {
- LabelPrivate *d = static_cast<LabelPrivate *>(d_ptr); // cast to our private type
- d->text = text;
- }
Como pueden ver, no es tan lindo tener static_cast por todos los lugares. En lugar de eso definimos macros.
- // global.h (macros)
- #define DPTR(Class) Class##Private *d = static_cast<Class##Private *>(d_ptr)
- #define QPTR(Class) Class *q = static_cast<Class *>(q_ptr)
- // label.cpp
- void Label::setText(const String &text) {
- DPTR(Label);
- d->text = text;
- }
- void LabelPrivate::someHelperFunction() {
- QPTR(label);
- q->selectAll(); // we can call functions in Label now
- }
d-pointers en Qt
En Qt, practicamente cada clase publica utiliza el enfoque d-pointer. Los unicos casos en donde no son usados es cuando se sabe en avance que la clase nunca tendra un miembro extra adicionado. Por ejemplo, para clases como QPoint, QRect, no se esperan agregar nuevos miembros y por tanto los data members estan guardados directo en la clase misma, en lugar de utilizar el d-pointer.
- En Qt, la clase base de todos los objetos privados es
QObjectPrivate. - Los macros
Q_DyQ_Qproveen la funcionalidad de QPTR y DPTR discutidos arriba. - Las clases de Qt tienen un macro
Q_DECLARE_PRIVATEen la clase publica. El macro hace esto:
- // qlabel.h
- private:
- };
- // 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;
La idea es que QLabel provea una funcion que permita a acceso a su clase interna privada. Este metodo por si mismo es privado (desde que el macro esta dentro de la seccion privada e d_func()QLabel.h). La d_func() puede sin embargoser invocada por friends (C++ friend) de QLabel. Esto es primaremente util el acceso a la informacion por las clasesde Qt que no pueden tener acceso de alguna informacion de QLabel utilizando la API publica. Como un bizarro ejemplo, QLabel puede mantener el rastro de cuantas veces el usuario ha hecho click en un link. Sin embargo, no hay un API publico para acceder a esa informacion. QStatistics es una clase que necesita esa informacion. Un desarrollador de Qt agregara QStatistics como amigo de QLabel y QStatistics podra entonces hacer label->d_func()->linkClickCount.
El d_func tambien tiene la ventaja de reforzar const-correctness: En una funcion const de MyClass necesitas un Q_D (const MyClass) y entonces solamente podras llamar funciones const en MyClassPrivate. Con un d_ptr “desnudo” tambien podras llamar funciones no-const.
Ademas tambien existe un Q_DECLARE_PUBLIC, el cual va hacia la otra direccion.

