c++ - ¿La fraseología de pImpl realmente se usa en la práctica?
oop pimpl-idiom (11)
Estoy leyendo el libro "Exceptional C ++" de Herb Sutter, y en ese libro aprendí sobre la expresión idiomática. Básicamente, la idea es crear una estructura para los objetos private
de una 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 ser cambiado 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 los proyectos de código abierto que he visto el código fuente. Entonces, me pregunto si esta técnica realmente se usa en la práctica.
¿Debo usarlo en todas partes o con precaución? ¿Y se recomienda esta técnica para ser utilizada en sistemas integrados (donde el rendimiento es muy importante)?
Entonces, me pregunto si esta técnica realmente se usa en la práctica. ¿Debo usarlo en todas partes o con precaución?
Por supuesto que se usa, y en mi proyecto, en casi todas las clases, por varias razones que mencionas:
- ocultamiento de datos
- el tiempo de recompilación realmente se reduce, ya que solo se debe reconstruir el archivo de origen, pero no el encabezado, y cada archivo que lo incluye
- compatibilidad binaria. Como la declaración de clase no cambia, es seguro actualizar la biblioteca (suponiendo que esté creando una biblioteca)
¿Se recomienda esta técnica para ser utilizada 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: mide y evalúa lo que ganas y pierdes.
Aquí hay un escenario real que encontré, donde este idioma ayudó mucho. Hace poco decidí admitir DirectX 11, así como mi compatibilidad con DirectX 9 existente, en un motor de juegos. El motor ya incluía la mayoría de las características DX, por lo que ninguna de las interfaces DX se usaba 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, como la semana como muchas otras extensiones. Si bien la mayoría de esas DLL no usaban DX directamente, requerían conocimiento y vinculación con DX simplemente porque sacaban los encabezados que exponían a DX. Al agregar DX 11, esta complejidad aumentaría drásticamente, aunque innecesariamente. Mover los miembros 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, mis interfaces expuestas se volvieron más limpias al mover las funciones de miembros privados al Pimpl, lo que expone solo las interfaces frontales.
Como muchos otros dijeron, la expresión Pimpl permite alcanzar la independencia completa de compilación y ocultación de información, lamentablemente con el costo de la pérdida de rendimiento (direccionamiento 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 integrado, en particular en aquellos escenarios donde la memoria debe ser economizada tanto como sea posible. El uso de clases abstractas de C ++ como interfaces daría los mismos beneficios al mismo costo. Esto muestra en realidad una gran deficiencia de C ++ donde, sin recurrir a las interfaces tipo C (métodos globales con un puntero opaco como parámetro), no es posible tener verdadera ocultación de información y compilación independiente sin inconvenientes de recursos adicionales: esto es principalmente porque el La declaración de una clase, que debe ser incluida por sus usuarios, exporta no solo la interfaz de la clase (métodos públicos) que necesitan los usuarios, sino también sus componentes internos (miembros privados), que los usuarios no necesitan.
Creo que esta es una de las herramientas más fundamentales para el desacoplamiento.
Estaba usando pimpl (y muchos otros modismos de Exceptional C ++) en el proyecto integrado (SetTopBox).
El propósito particular de este idoim en nuestro proyecto fue ocultar los tipos que usa la clase XImpl. Específicamente, lo usamos para ocultar los detalles de las implementaciones para diferentes hardware, donde se incorporarí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.
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 instanciación de la plantilla requiere la declaración completa disponible donde tuvo lugar la instanciación. (Y esa es la razón principal por la que no ve métodos de plantilla definidos en archivos CPP)
Todavía puede referirse a las subclases Templetised, pero como debe incluirlas todas, se pierde toda ventaja de la "desacoplamiento de la implementación" en la compilación (evitando incluir todo el código específico de plataforma en todas partes, acortando la compilación).
Es un buen paradigma para OOP clásico (basado en herencia) pero no para programación genérica (basada en especialización).
Otras personas ya han proporcionado las ventajas / desventajas técnicas, pero creo que vale la pena mencionar lo siguiente:
En primer lugar, no seas dogmático. Si pImpl funciona para su situación, úselo - simplemente no lo use porque "es mejor OO porque oculta la implementación", etc. Citando las preguntas frecuentes de C ++:
la encapsulación es para código, no para personas ( source )
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 (por ejemplo, <Thread.h>
) todos los códigos específicos de la plataforma, porque las variables de estado internas (por ejemplo, los identificadores de subprocesos) difieren de una plataforma a otra. De esta forma, uno puede compilar código en contra de su biblioteca sin ningún conocimiento de la idiosincrasia de las otras plataformas, porque todo está oculto.
Parece que muchas bibliotecas lo usan para mantenerse estable en su API, al menos para algunas versiones.
Pero en cuanto a todas las cosas, nunca debes usar nada en todas partes sin precaución. Siempre piensa antes de usarlo. Evalúe qué ventajas le ofrece, y si valen el precio que paga.
Las ventajas que puede darte son:
- ayuda a mantener la compatibilidad binaria de bibliotecas compartidas
- escondiendo ciertos detalles internos
- ciclos de recompilación decrecientes
Esos pueden o no ser ventajas reales para usted. Me gusta, no me importa unos minutos de tiempo de recompilación. Los usuarios finales generalmente tampoco, ya que siempre 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 en el 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 alinear cosas como lo hace con una implementación ingenua de su clase)
Así que, cuidadosamente, déle valor a todo y evalúelo usted mismo. Para mí, casi siempre resulta que usar el idioma pimpl no vale la pena el esfuerzo. Solo hay un caso en el que lo uso personalmente (o al menos algo similar):
Mi contenedor de C ++ para la llamada de stat
Linux. Aquí la estructura del encabezado C puede ser diferente, dependiendo de qué #defines
se establezcan. Y como mi encabezado de contenedor no puede controlarlos a todos, solo #include <sys/stat.h>
en mi archivo .cxx
y evito estos problemas.
Principalmente consideraría PIMPL para las clases expuestas para ser utilizado 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 es necesario recompilarlos 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 estos beneficios tienen un costo: se requiere un nivel extra de indirección para acceder a los métodos de implementación.
Se usa 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 usa esto es Qt , donde la idea básica es ocultar la implementación o el código específico de plataforma del usuario (otros desarrolladores que usan Qt).
Esta es una idea noble, pero hay una verdadera desventaja: depuración. Siempre que el código oculto en implementaciones privadas sea de calidad superior, todo está bien, pero si hay errores, entonces el usuario / desarrollador tiene un problema. porque es solo un indicador tonto de 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 ventajas y desventajas.
Solía usar esta técnica mucho en el pasado, pero luego me encontré alejándome de ella.
Por supuesto, es una buena idea esconder los detalles de implementación lejos de los usuarios de su clase. Sin embargo, también puede hacerlo al hacer 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) tal que varias clases accedan al mismo "impl", los usuarios del módulo solo usarán las clases "expuestas".
No v-table si se supone que 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 de unidades.
(El mayor problema). Antes de los días de unique_ptr y moving, tenía opciones restringidas sobre cómo almacenar el pImpl. Un puntero sin formato y tienes problemas con respecto a que tu clase no se puede copiar. Un viejo auto_ptr no funcionaría con la clase declarada con anticipación (no en todos los compiladores de todos modos). Entonces, la gente comenzó a usar shared_ptr, lo que fue bueno para hacer que tu clase se pudiera 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 solía ser utilizar un puntero sin formato para el interno y hacer que la clase no se pueda copiar y, en su lugar, devolver un shared_ptr. Entonces dos llamadas a nuevas. (En realidad, 3 dado old shared_ptr te dio un segundo).
Técnicamente, no es realmente correcto, ya que la constness no se propaga a través de un puntero de miembro.
En general, por lo tanto, me he alejado en los años de pImpl y en el uso de interfaz abstracta en su lugar (y los métodos de fábrica para crear instancias).
Un beneficio que puedo ver es que 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 entendiendo la semántica del movimiento.