¿Se utiliza realmente el idioma de pImpl en la práctica?

165

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 privateobjetos de a classy 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)?

Renan Greinert
fuente
¿Es esto esencialmente lo mismo que decidir que X es una interfaz (abstracta) y Ximpl es la implementación? struct XImpl : public X. Eso me parece más natural. ¿Hay algún otro problema que me haya perdido?
Aaron McDaid el
@AaronMcDaid: es similar, pero tiene las ventajas de que (a) las funciones miembro no tienen que ser virtuales, y (b) no necesita una fábrica o la definición de la clase de implementación para crear una instancia.
Mike Seymour el
2
@AaronMcDaid El modismo de pimpl evita las llamadas a funciones virtuales. También es un poco más C ++ - ish (para alguna concepción de C ++ - ish); invocas constructores, en lugar de funciones de fábrica. He usado ambos, dependiendo de lo que haya en la base de código existente: el modismo de pimpl (originalmente llamado mod de gato de Cheshire y anterior a la descripción de Herb por al menos 5 años) parece tener una historia más larga y ser más ampliamente utilizado en C ++, pero por lo demás, ambos funcionan.
James Kanze el
30
En C ++, pimpl debe implementarse con const unique_ptr<XImpl>más que con XImpl*.
Neil G
1
"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". Qt casi nunca lo usa NO.
ManuelSchneid3r

Respuestas:

132

Entonces, me pregunto si esta técnica se usa realmente en la práctica. ¿Debo usarlo en todas partes o con precaución?

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 XImplsin romper la compatibilidad binaria con su cliente (¡lo que significaría fallas!). Dado que el diseño binario de la Xclase no cambia cuando agrega nuevos campos a la Ximplclase, 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/ XImplsin 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) Xdebe reconstruirse cuando agrega / elimina campos y / o métodos a la XImplclase (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 asigna X(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.cppdebe volver a compilar incluso cuando X::foo()se agregó Xy X.hcambió un método privado , ¡aunque XClient1.cppno 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.


¿Se recomienda utilizar esta técnica en sistemas integrados (donde el rendimiento es muy importante)?

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!

BЈовић
fuente
16
+1 porque también es ampliamente utilizado en la empresa para la que trabajo, y por las mismas razones.
Benoit
9
también, compatibilidad binaria
Ambroz Bizjak
9
En la biblioteca Qt, este método también se utiliza en situaciones de puntero inteligente. Entonces QString mantiene su contenido como una clase inmutable internamente. Cuando la clase pública se "copia", el puntero del miembro privado se copia en lugar de toda la clase privada. Estas clases privadas también usan punteros inteligentes, por lo que básicamente obtienes recolección de basura con la mayoría de las clases, además del rendimiento mejorado debido a la copia del puntero en lugar de la copia de la clase completa
Timothy Baldridge
8
Aún más, con el lenguaje pimpl Qt puede mantener la compatibilidad binaria hacia adelante y hacia atrás dentro de una sola versión principal (en la mayoría de los casos). OMI, esta es, con mucho, la razón más importante para usarlo.
whitequark
1
También es útil para implementar código específico de la plataforma, ya que puede conservar la misma API.
doc
49

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:

  • ayuda a mantener la compatibilidad binaria de las bibliotecas compartidas
  • ocultando ciertos detalles internos
  • disminuyendo los ciclos de recompilación

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):

  • Aumento del uso de memoria debido a más asignaciones que con la variante ingenua
  • mayor esfuerzo de mantenimiento (debe escribir al menos las funciones de reenvío)
  • pérdida de rendimiento (es posible que el compilador no pueda insertar cosas en línea, ya que es con una implementación ingenua de su clase)

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 statllamada de Linux . Aquí la estructura del encabezado C puede ser diferente, dependiendo de lo que #definesse establezca. Y dado que mi encabezado de contenedor no puede controlarlos a todos, solo #include <sys/stat.h>en mi .cxxarchivo y evito estos problemas.

PlasmaHH
fuente
2
Casi siempre debe usarse para las interfaces del sistema, para hacer que el sistema de código de la interfaz sea independiente. Mi Fileclase (que expone que gran parte de la información statregresaría en Unix) usa la misma interfaz en Windows y Unix, por ejemplo.
James Kanze el
55
@JamesKanze: Incluso allí, personalmente, primero me sentaría por un momento y pensaría si tal vez no es suficiente tener unos #ifdefsegundos 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.
PlasmaHH
31

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).

Emilio Garavaglia
fuente
44
Debe ser más preciso: no hay absolutamente ningún problema al usar clases PIMPL como argumentos de tipo plantilla. Solo si la clase de implementación en sí misma necesita ser parametrizada en los argumentos de plantilla de la clase externa, ya no se puede ocultar del encabezado de la interfaz, incluso si todavía es una clase privada. Si puede eliminar el argumento de la plantilla, ciertamente aún puede hacer PIMPL "apropiado". Con la eliminación de tipos, también puede hacer el PIMPL en una clase base que no sea de plantilla y luego hacer que la clase de plantilla se derive de ella.
Vuelva a instalar Monica
22

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 ++:

la encapsulación es para código, no para personas ( fuente )

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.

azalea
fuente
12

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.

Ghita
fuente
"Se requiere un nivel adicional de indirección para acceder a los métodos de implementación". ¿Es?
xaxxon
@xaxxon sí, lo es. pimpl es más lento si los métodos son de bajo nivel. nunca lo use para cosas que viven en un circuito cerrado, por ejemplo.
Erik Aronesty
@xaxxon Yo diría que, en general, se requiere un nivel adicional. Si se realiza la alineación, entonces no. Pero la incorporación no sería una opción en el código compilado en un dll diferente.
Ghita
5

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.

usuario377178
fuente
4

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:

  1. Suponiendo que solo hay una implementación de esta interfaz, es más clara al no usar la clase abstracta / implementación concreta

  2. 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".

  3. No hay v-table si se supone que esto es algo malo.

Las desventajas que encontré de pImpl (donde la interfaz abstracta funciona mejor)

  1. 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.

  2. (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).

  3. 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).

CashCow
fuente
3

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,

ncsc
fuente
3

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,

Kit10
fuente
2

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.

Holger Kretzschmar
fuente
9
es tonto pero está escrito ... ¿por qué no puede seguir el código en el depurador?
UncleZeiv
2
En términos generales, para depurar el código Qt necesita construir Qt usted mismo. Una vez que lo haga, no hay problema en ingresar a los métodos PIMPL e inspeccionar el contenido de los datos PIMPL.
Restablecer Monica
0

Un beneficio que puedo ver es que le permite al programador implementar ciertas operaciones de una manera bastante rápida:

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PD: Espero no estar malentendiendo la semántica de movimientos.

BenGoldberg
fuente