Estoy leyendo el libro "Excepcional C ++" de Herb Sutter, y en ese libro he aprendido sobre el lenguaje de ejemplo. Básicamente, la idea es crear una estructura para los private
objetos de a class
y asignarlos dinámicamente para disminuir el tiempo de compilación (y también ocultar las implementaciones privadas de una mejor manera).
Por ejemplo:
class X
{
private:
C c;
D d;
} ;
podría cambiarse a:
class X
{
private:
struct XImpl;
XImpl* pImpl;
};
y, en el CPP, la definición:
struct X::XImpl
{
C c;
D d;
};
Esto parece bastante interesante, pero nunca antes había visto este tipo de enfoque, ni en las empresas en las que he trabajado, ni en proyectos de código abierto en los que he visto el código fuente. Entonces, me pregunto si esta técnica se usa realmente en la práctica.
¿Debo usarlo en todas partes o con precaución? ¿Y se recomienda utilizar esta técnica en sistemas integrados (donde el rendimiento es muy importante)?
fuente
struct XImpl : public X
. Eso me parece más natural. ¿Hay algún otro problema que me haya perdido?const unique_ptr<XImpl>
más que conXImpl*
.Respuestas:
Por supuesto que se usa. Lo uso en mi proyecto, en casi todas las clases.
Razones para usar el idioma PIMPL:
Compatibilidad binaria
Cuando está desarrollando una biblioteca, puede agregar / modificar campos
XImpl
sin romper la compatibilidad binaria con su cliente (¡lo que significaría fallas!). Dado que el diseño binario de laX
clase no cambia cuando agrega nuevos campos a laXimpl
clase, es seguro agregar nueva funcionalidad a la biblioteca en actualizaciones de versiones menores.Por supuesto, también puede agregar nuevos métodos no virtuales públicos / privados para
X
/XImpl
sin romper la compatibilidad binaria, pero eso está a la par con la técnica estándar de encabezado / implementación.Ocultar datos
Si está desarrollando una biblioteca, especialmente una propiedad, puede ser conveniente no revelar qué otras bibliotecas / técnicas de implementación se usaron para implementar la interfaz pública de su biblioteca. Ya sea por problemas de propiedad intelectual o porque cree que los usuarios podrían verse tentados a asumir suposiciones peligrosas sobre la implementación o simplemente romper la encapsulación mediante el uso de trucos de lanzamiento terribles. PIMPL resuelve / mitiga eso.
Tiempo de compilación
El tiempo de compilación disminuye, ya que solo el archivo de origen (implementación)
X
debe reconstruirse cuando agrega / elimina campos y / o métodos a laXImpl
clase (que se asigna para agregar campos / métodos privados en la técnica estándar). En la práctica, es una operación común.Con la técnica estándar de encabezado / implementación (sin PIMPL), cuando agrega un nuevo campo a
X
, cada cliente que asignaX
(ya sea en la pila o en el montón) debe volver a compilarse, ya que debe ajustar el tamaño de la asignación. Bueno, todos los clientes que nunca asignan X también deben volver a compilarse, pero son solo gastos generales (el código resultante en el lado del cliente será el mismo).Además, con la separación estándar de encabezado / implementación, se
XClient1.cpp
debe volver a compilar incluso cuandoX::foo()
se agregóX
yX.h
cambió un método privado , ¡aunqueXClient1.cpp
no es posible llamar a este método por razones de encapsulación! Al igual que arriba, es pura sobrecarga y está relacionado con la forma en que funcionan los sistemas de compilación C ++ de la vida real.Por supuesto, la recompilación no es necesaria cuando solo modifica la implementación de los métodos (porque no toca el encabezado), pero eso está a la par con la técnica estándar de encabezado / implementación.
Eso depende de cuán poderoso sea tu objetivo. Sin embargo, la única respuesta a esta pregunta es: medir y evaluar lo que gana y pierde. Además, tenga en cuenta que si no está publicando una biblioteca destinada a ser utilizada en sistemas integrados por sus clientes, ¡solo se aplica la ventaja de tiempo de compilación!
fuente
Parece que muchas bibliotecas lo utilizan para mantenerse estable en su API, al menos para algunas versiones.
Pero como para todas las cosas, nunca debes usar nada en todas partes sin precaución. Siempre piense antes de usarlo. Evalúe qué ventajas le brinda y si valen la pena el precio que paga.
Las ventajas que puede brindarle son:
Esas pueden o no ser ventajas reales para usted. Como a mí, no me importan unos minutos de tiempo de recompilación. Los usuarios finales generalmente tampoco lo hacen, ya que siempre lo compilan una vez y desde el principio.
Las posibles desventajas son (también aquí, dependiendo de la implementación y si son desventajas reales para usted):
Por lo tanto, valore cuidadosamente todo y evalúelo usted mismo. Para mí, casi siempre resulta que no vale la pena usar el lenguaje de pimpl. Solo hay un caso en el que lo uso personalmente (o al menos algo similar):
Mi contenedor C ++ para la
stat
llamada de Linux . Aquí la estructura del encabezado C puede ser diferente, dependiendo de lo que#defines
se establezca. Y dado que mi encabezado de contenedor no puede controlarlos a todos, solo#include <sys/stat.h>
en mi.cxx
archivo y evito estos problemas.fuente
File
clase (que expone que gran parte de la informaciónstat
regresaría en Unix) usa la misma interfaz en Windows y Unix, por ejemplo.#ifdef
segundos para hacer que el envoltorio sea lo más delgado posible. Pero cada uno tiene objetivos diferentes, lo importante es tomarse el tiempo para pensarlo en lugar de seguir ciegamente algo.Estoy de acuerdo con todos los demás sobre los productos, pero permítanme poner en evidencia un límite: no funciona bien con las plantillas .
La razón es que la creación de instancias de plantilla requiere la declaración completa disponible donde tuvo lugar la creación de instancias. (Y esa es la razón principal por la que no ve los métodos de plantilla definidos en los archivos CPP)
Todavía puede referirse a las subclases templetizadas, pero como debe incluirlas todas, se pierden todas las ventajas del "desacoplamiento de implementación" en la compilación (evitando incluir todos los códigos específicos de plataformas en todas partes, acortando la compilación).
Es un buen paradigma para la OOP clásica (basada en la herencia) pero no para la programación genérica (basada en la especialización).
fuente
Otras personas ya han brindado las ventajas y desventajas técnicas, pero creo que vale la pena señalar lo siguiente:
En primer lugar, no seas dogmático. Si pImpl funciona para su situación, úselo; no lo use solo porque "es mejor OO ya que realmente oculta la implementación", etc. Citando las preguntas frecuentes de C ++:
Solo para darle un ejemplo de software de código abierto donde se usa y por qué: OpenThreads, la biblioteca de subprocesos utilizada por OpenSceneGraph . La idea principal es eliminar del encabezado (p
<Thread.h>
. Ej. ) Todo el código específico de la plataforma, porque las variables de estado interno (p. Ej., Identificadores de subprocesos) difieren de una plataforma a otra. De esta manera, uno puede compilar código en su biblioteca sin ningún conocimiento de las idiosincrasias de las otras plataformas, porque todo está oculto.fuente
Consideraría principalmente PIMPL para las clases expuestas para ser utilizadas como API por otros módulos. Esto tiene muchos beneficios, ya que hace que la recopilación de los cambios realizados en la implementación de PIMPL no afecte al resto del proyecto. Además, para las clases API, promueven una compatibilidad binaria (los cambios en la implementación de un módulo no afectan a los clientes de esos módulos, no tienen que volver a compilarse ya que la nueva implementación tiene la misma interfaz binaria: la interfaz expuesta por el PIMPL).
En cuanto al uso de PIMPL para cada clase, consideraría precaución porque todos esos beneficios tienen un costo: se requiere un nivel adicional de indirección para acceder a los métodos de implementación.
fuente
Creo que esta es una de las herramientas más fundamentales para desacoplar.
Estaba usando pimpl (y muchos otros modismos de Exceptional C ++) en un proyecto incrustado (SetTopBox).
El propósito particular de esta idoim en nuestro proyecto era ocultar los tipos que usa la clase XImpl. Específicamente lo usamos para ocultar detalles de implementaciones para diferentes hardware, donde se colocarían diferentes encabezados. Tuvimos diferentes implementaciones de clases XImpl para una plataforma y diferentes para la otra. El diseño de la clase X se mantuvo igual independientemente de la plataforma.
fuente
Solía usar esta técnica mucho en el pasado, pero luego me encontré alejándome de ella.
Por supuesto, es una buena idea ocultar los detalles de implementación lejos de los usuarios de su clase. Sin embargo, también puede hacerlo haciendo que los usuarios de la clase utilicen una interfaz abstracta y que los detalles de implementación sean la clase concreta.
Las ventajas de pImpl son:
Suponiendo que solo hay una implementación de esta interfaz, es más clara al no usar la clase abstracta / implementación concreta
Si tiene un conjunto de clases (un módulo) de modo que varias clases accedan al mismo "impl", pero los usuarios del módulo solo usarán las clases "expuestas".
No hay v-table si se supone que esto es algo malo.
Las desventajas que encontré de pImpl (donde la interfaz abstracta funciona mejor)
Si bien es posible que solo tenga una implementación de "producción", al usar una interfaz abstracta también puede crear una implementación "simulada" que funcione en pruebas unitarias.
(El mayor problema). Antes de los días de unique_ptr y mudanza, tenía opciones restringidas sobre cómo almacenar el pImpl. Un puntero en bruto y tenía problemas sobre que su clase no se podía copiar. Un auto_ptr antiguo no funcionaría con la clase declarada hacia adelante (de todos modos, no en todos los compiladores). Entonces, la gente comenzó a usar shared_ptr, lo que fue bueno para hacer que tu clase se pueda copiar, pero por supuesto ambas copias tenían el mismo shared_ptr subyacente que no podrías esperar (modifica uno y ambos se modifican). Por lo tanto, la solución a menudo era usar un puntero sin procesar para el interno y hacer que la clase no se pueda copiar y devolver un shared_ptr a eso. Entonces dos llamadas a nuevo. (En realidad 3 dado old shared_ptr le dio un segundo).
Técnicamente no es realmente constante, ya que la constante no se propaga a un puntero miembro.
En general, por lo tanto, me he alejado en los años de pImpl y en su lugar uso de interfaz abstracta (y métodos de fábrica para crear instancias).
fuente
Como muchos otros dijeron, el idioma Pimpl permite alcanzar información completa ocultando y compilando la independencia, desafortunadamente con el costo de la pérdida de rendimiento (indirección adicional del puntero) y la necesidad de memoria adicional (el puntero del miembro en sí). El costo adicional puede ser crítico en el desarrollo de software embebido, en particular en aquellos escenarios donde la memoria se debe economizar tanto como sea posible. El uso de clases abstractas C ++ como interfaces conduciría a los mismos beneficios al mismo costo. Esto muestra en realidad una gran deficiencia de C ++ donde, sin recurrir a interfaces similares a C (métodos globales con un puntero opaco como parámetro), no es posible ocultar la información verdadera y la independencia de compilación sin inconvenientes de recursos adicionales: esto se debe principalmente a que declaración de una clase, que debe ser incluida por sus usuarios,
fuente
Aquí hay un escenario real que encontré, donde este idioma ayudó mucho. Recientemente decidí admitir DirectX 11, así como mi compatibilidad actual con DirectX 9, en un motor de juego. El motor ya incluía la mayoría de las funciones DX, por lo que ninguna de las interfaces DX se utilizó directamente; solo se definieron en los encabezados como miembros privados. El motor utiliza archivos DLL como extensiones, agregando teclado, mouse, joystick y soporte de secuencias de comandos, al igual que muchas otras extensiones. Si bien la mayoría de esas DLL no usaban DX directamente, requerían conocimiento y vinculación a DX simplemente porque introducían encabezados que exponían DX. Al agregar DX 11, esta complejidad aumentaría dramáticamente, aunque innecesariamente. Mover los miembros de DX a un Pimpl definido solo en la fuente eliminó esta imposición. Además de esta reducción de las dependencias de la biblioteca,
fuente
Se utiliza en la práctica en muchos proyectos. Su utilidad depende en gran medida del tipo de proyecto. Uno de los proyectos más destacados que usan esto es Qt , donde la idea básica es ocultar la implementación o el código específico de la plataforma del usuario (otros desarrolladores que usan Qt).
Esta es una idea noble, pero tiene un inconveniente real: depuración Siempre que el código oculto en las implementaciones privadas sea de calidad premium, todo está bien, pero si hay errores allí, entonces el usuario / desarrollador tiene un problema, porque es solo un puntero tonto a una implementación oculta, incluso si tiene el código fuente de las implementaciones.
Entonces, como en casi todas las decisiones de diseño, hay pros y contras.
fuente
Un beneficio que puedo ver es que le permite al programador implementar ciertas operaciones de una manera bastante rápida:
PD: Espero no estar malentendiendo la semántica de movimientos.
fuente