c++ - ¿Por qué debería usarse el modismo "PIMPL"?
oop information-hiding (11)
Acabo de implementar mi primera clase de pimpl en los últimos días. Lo usé para eliminar los problemas que tenía incluyendo winsock2.h en Borland Builder. Parecía estar alterando la alineación de estructuras y dado que tenía datos de socket en la clase privada, esos problemas se estaban extendiendo a cualquier archivo cpp que incluyera el encabezado.
Al usar pimpl, winsock2.h se incluyó en un solo archivo cpp en el que pude ponerle un cierre al problema y no preocuparme de que me volviera a picar.
Para responder a la pregunta original, la ventaja que encontré al reenviar las llamadas a la clase de los pimplos fue que la clase pimpl es la misma que tu clase original antes de que lo hicieras, además tus implementaciones no se extienden por 2 clases de alguna manera extraña. Es mucho más claro implementar al público para simplemente reenviar a la clase de pimpl.
Como dijo el señor Nodet, una clase, una responsabilidad.
Esta pregunta ya tiene una respuesta aquí:
Antecedentes:
El Idioma de PIMPL (Pointer to IMPLementation) es una técnica para la ocultación de la implementación en la que una clase pública envuelve una estructura o clase que no se puede ver fuera de la biblioteca de la que forma parte la clase pública.
Esto oculta detalles de implementación interna y datos del usuario de la biblioteca.
Al implementar este modismo, ¿por qué colocaría los métodos públicos en la clase pimpl y no en la clase pública ya que las implementaciones del método de clases públicas se compilarían en la biblioteca y el usuario solo tiene el archivo de encabezado?
Para ilustrarlo, este código coloca la implementación de Purr()
en la clase impl y la envuelve también.
¿Por qué no implementar Purr directamente en la clase pública?
// header file:
class Cat {
private:
class CatImpl; // Not defined here
CatImpl *cat_; // Handle
public:
Cat(); // Constructor
~Cat(); // Destructor
// Other operations...
Purr();
};
// CPP file:
#include "cat.h"
class Cat::CatImpl {
Purr();
... // The actual implementation can be anything
};
Cat::Cat() {
cat_ = new CatImpl;
}
Cat::~Cat() {
delete cat_;
}
Cat::Purr(){ cat_->Purr(); }
CatImpl::Purr(){
printf("purrrrrr");
}
Bueno, no lo usaría. Tengo una mejor alternativa:
foo.h:
class Foo {
public:
virtual ~Foo() { }
virtual void someMethod() = 0;
// This "replaces" the constructor
static Foo *create();
}
foo.cpp:
namespace {
class FooImpl: virtual public Foo {
public:
void someMethod() {
//....
}
};
}
Foo *Foo::create() {
return new FooImpl;
}
¿Este patrón tiene un nombre?
Como programador de Python y Java, me gusta mucho más que el idioma de pImpl.
Creo que la mayoría de la gente se refiere a esto como la expresión de Handle Body. Véase el libro de James Coplien, Estilos y lenguajes avanzados de programación en C ++ ( enlace de Amazon ). También es conocido como el Gato de Cheshire debido al personaje de Lewis Caroll que se desvanece hasta que solo queda la sonrisa.
El código de ejemplo se debe distribuir a través de dos conjuntos de archivos fuente. Entonces solo Cat.h es el archivo que se envía con el producto.
CatImpl.h está incluido en Cat.cpp y CatImpl.cpp contiene la implementación de CatImpl :: Purr (). Esto no será visible para el público que usa su producto.
Básicamente, la idea es ocultar la mayor parte posible de la implementación desde miradas indiscretas. Esto es más útil cuando tiene un producto comercial que se envía como una serie de bibliotecas a las que se accede a través de una API a la que se compila y se vincula el código del cliente.
Hicimos esto con la reescritura del producto IONAs Orbix 3.3 en 2000.
Como lo mencionaron otros, usar su técnica desacopla por completo la implementación de la interfaz del objeto. Entonces no tendrá que volver a compilar todo lo que usa Cat si solo quiere cambiar la implementación de Purr ().
Esta técnica se usa en una metodología llamada diseño por contrato .
Encuentro contundente que, a pesar de lo conocido que es el lenguaje pimpl, no creo que surja muy a menudo en la vida real (por ejemplo, en proyectos de código abierto).
A menudo me pregunto si los "beneficios" son exagerados; sí, puede hacer que algunos de sus detalles de implementación estén aún más ocultos, y sí, puede cambiar su implementación sin cambiar el encabezado, pero no es obvio que estas sean grandes ventajas en la realidad.
Es decir, no está claro que haya una necesidad de que su implementación esté tan bien escondida, y tal vez sea bastante raro que las personas realmente cambien solo la implementación; tan pronto como necesite agregar nuevos métodos, digamos, necesita cambiar el encabezado de todos modos.
No sé si esta es una diferencia que vale la pena mencionar, pero ...
¿Sería posible tener la implementación en su propio espacio de nombres y tener un espacio de nombres público de contenedor / biblioteca para el código que el usuario ve?
catlib::Cat::Purr(){ cat_->Purr(); }
cat::Cat::Purr(){
printf("purrrrrr");
}
De esta forma, todo el código de la biblioteca puede hacer uso del espacio de nombres cat y, como se presenta la necesidad de exponer una clase al usuario, se podría crear un contenedor en el espacio de nombres de catlib.
Normalmente, la única referencia a la clase Pimpl en el encabezado de la clase Owner (Cat en este caso) sería una declaración forward, como lo ha hecho aquí, porque eso puede reducir las dependencias.
Por ejemplo, si su clase Pimpl tiene ComplicatedClass como miembro (y no solo un puntero o referencia), necesitaría tener ComplicatedClass completamente definido antes de su uso. En la práctica, esto significa incluir "ComplicatedClass.h" (que también incluirá indirectamente todo aquello en lo que ComplicatedClass dependa). Esto puede llevar a un único encabezado llenando muchas cosas, lo cual es malo para administrar sus dependencias (y sus tiempos de compilación).
Cuando utilizas el idpl pimpl, solo necesitas #incluir el material utilizado en la interfaz pública de tu tipo Owner (que sería Cat aquí). Lo que mejora las cosas para las personas que usan su biblioteca, y significa que no tiene que preocuparse por las personas que dependen de alguna parte interna de su biblioteca, ya sea por error o porque quieren hacer algo que no permite, por lo que #define público privado antes de incluir sus archivos.
Si se trata de una clase simple, generalmente no hay razón para usar un Pimpl, pero para los tiempos en que los tipos son bastante grandes, puede ser una gran ayuda (especialmente para evitar tiempos de construcción largos).
Por lo que vale, separa la implementación de la interfaz. Esto generalmente no es muy importante en proyectos de pequeño tamaño. Pero, en proyectos grandes y bibliotecas, se puede usar para reducir significativamente los tiempos de construcción.
Tenga en cuenta que la implementación de Cat
puede incluir muchos encabezados, puede implicar una metaprogramación de plantillas que lleva tiempo compilar por sí misma. ¿Por qué debería un usuario, que solo quiere usar el Cat
incluir todo eso? Por lo tanto, todos los archivos necesarios se ocultan utilizando el idioma pimpl (de ahí la declaración directa de CatImpl
), y el uso de la interfaz no obliga al usuario a incluirlos.
Estoy desarrollando una biblioteca para la optimización no lineal (leer "muchas matemáticas desagradables"), que se implementa en plantillas, por lo que la mayoría del código está en los encabezados. Se tarda unos cinco minutos en compilar (en una CPU de varios núcleos decente), y solo analizar los encabezados en un .cpp
otro modo .cpp
vacío lleva alrededor de un minuto. Entonces, cualquiera que use la biblioteca tiene que esperar un par de minutos cada vez que compilan su código, lo que hace que el desarrollo sea bastante tedious . Sin embargo, al ocultar la implementación y los encabezados, uno solo incluye un archivo de interfaz simple, que se compila al instante.
No necesariamente tiene que ver con proteger la implementación para que no sea copiada por otras compañías, lo que probablemente no ocurriría de todas maneras, a menos que el funcionamiento interno de su algoritmo pueda adivinarse a partir de las definiciones de las variables miembro (si es así, es probablemente no sea muy complicado y no vale la pena protegerlo en primer lugar).
Realizar la llamada a la impl-> Purr dentro del archivo cpp significa que en el futuro podría hacer algo completamente diferente sin tener que cambiar el archivo de encabezado. Tal vez el próximo año descubran un método de ayuda que podrían haber llamado en su lugar y así pueden cambiar el código para llamarlo directamente y no usar impl-> Purr en absoluto. (Sí, podrían lograr lo mismo actualizando también el método real impl :: Purr, pero en ese caso se quedará bloqueado con una llamada de función adicional que no logrará nada más que llamar a la siguiente función sucesivamente)
También significa que el encabezado solo tiene definiciones y no tiene ninguna implementación que permita una separación más clara, que es el objetivo de la expresión idiomática.
Si su clase utiliza el modismo pimpl, puede evitar cambiar el archivo de encabezado en la clase pública.
Esto le permite agregar / eliminar métodos a la clase pimpl, sin modificar el archivo de encabezado de la clase externa. También puede agregar / eliminar #includes al pimpl también.
Cuando cambia el archivo de encabezado de la clase externa, tiene que volver a compilar todo lo que #include (y si alguno de ellos son archivos de encabezado, debe recompilar todo lo que #incluye, etc.)
Usamos lenguaje PIMPL para emular la programación orientada a aspectos donde los aspectos pre, post y error son llamados antes y después de la ejecución de una función miembro.
struct Omg{
void purr(){ cout<< "purr/n"; }
};
struct Lol{
Omg* omg;
/*...*/
void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } }
};
También usamos puntero a la clase base para compartir diferentes aspectos entre muchas clases.
El inconveniente de este enfoque es que el usuario de la biblioteca debe tener en cuenta todos los aspectos que se van a ejecutar, pero solo ve su clase. Requiere examinar la documentación para detectar cualquier efecto secundario.
- Porque quiere que
Purr()
pueda usar miembros privados deCatImpl
.Cat::Purr()
no se le permitiría tal acceso sin una declaración defriend
. - Porque entonces no mezclas responsabilidades: una clase implementa, una clase adelante.