2008-09-13 13 views
104

Datos:¿Por qué debería usarse el modismo "PIMPL"?

El PIMPL Idiom (puntero a la implementación) es una técnica para ocultar la aplicación en la que una clase pública envuelve una estructura o clase que no puede ser visto fuera de la biblioteca de la clase pública es parte de.

Esto oculta los detalles de implementación interna y los 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 ilustrar, este código pone la implementación Purr() en la clase impl y lo 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"); 
} 
+26

Porque se debe evitar el modismo PIMP? .. – mlvljr

+1

Excelente respuesta, y encontré que este enlace contiene información completa también: http://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl/ – zhanxw

+11

If Si desea hacer un codificador de mantenimiento después de un favor, recuerde que este es un patrón de ** interfaz **. No lo use para cada clase interna que hay. Para citar a Blade Runner, he visto cosas que tu gente no creería. – DevSolar

Respuesta

34
  • Debido Quiere Purr() para poder usar miembros privados de CatImpl. Cat::Purr() no se le permitiría tal acceso sin una declaración friend.
  • Porque entonces no se mezclan responsabilidades: una clase implementa, una clase adelante.
+4

Aunque es un dolor mantener. Pero, de nuevo, si se trata de una clase de biblioteca, los métodos no deberían cambiarse demasiado de todos modos. El código que estoy buscando parece tomar el camino seguro y usar pimpl en todas partes. – JeffV

+0

¿No es esta línea ilegal ya que todos los miembros son privados: cat _-> Purr(); Purr() no es accesible desde el exterior porque por deafult es privado. ¿Que me estoy perdiendo aqui? – binaryguy

+0

Ambos puntos no tienen ningún sentido. Si tuviera una clase, 'Cat', también podría acceder a sus miembros y no podría" mezclar reposibilidades ", porque sería la clase la que" implementa ". Las razones para usar PIMPL son diferentes. – doc

2

La colocación de la llamada a la impl-> Purr dentro del archivo CPP significa que en el futuro se podría hacer algo completamente diferente sin tener que cambiar el archivo de cabecera. 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í, también podrían lograr lo mismo actualizando el método impl :: Purr real, pero en ese caso está atrapado con una llamada de función adicional que no logra nada más que llamar a la siguiente función)

También significa el encabezado solo tiene definiciones y no tiene ninguna implementación que permita una separación más limpia, que es el objetivo de la expresión idiomática.

50

Creo que la mayoría de las personas se refieren a esto como la expresión de Handle Body. Consulte el libro de James Coplien, Estilos y lenguajes avanzados de programación en C++ (Amazon link). También se conoce como Cheshire Cat debido al personaje de Lewis Caroll que se desvanece hasta que solo queda la sonrisa.

El código de ejemplo se debe distribuir entre 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 tanto como sea posible la implementación de los ojos. 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 que compila y enlaza el código del cliente.

Lo hemos hecho con la reescritura de Ionaş Orbix 3.3 del producto en 2000.

como han mencionado otros, usando su técnica permite separar 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 utiliza en una metodología llamada design by contract.

+4

¿Cómo se usa el diseño de pimpl (o el idioma del mango y el cuerpo) en el diseño por contrato? –

+0

Hola, Andreas, su punto de interfaz para un usuario API solo está en el contrato expuesto (identificador) y no en la forma en que implementa el cuerpo para proporcionar la funcionalidad publicitada. Usted es libre de cambiar la implementación como mejor le parezca, siempre que no cambie la semántica de la API que ha anunciado. –

+0

@Rob, supongo que hay muy poco sobre esto. Una clase extra, pero hay muy poco para ellos. Solo una capa delgada alrededor de la clase existente. Alguien me corrige si estoy equivocado, pero el uso de la memoria sería simplemente una tabla de funciones extra en RAM, un puntero al pimpl y una función de redireccionamiento para cada método en el espacio de código. Doloroso para el mantenimiento y la depuración sin embargo. – JeffV

6

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 en gran medida las dependencias.

Por ejemplo, si su clase Pimpl tiene ComplicatedClass como miembro (y no solo un puntero o referencia), necesitará tener ComplicatedClass completamente definida 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 utiliza el pimpl idion, solo necesita #incluir las cosas utilizadas en la interfaz pública de su tipo de propietario (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, por lo general hay ninguna razón para utilizar un Pimpl, pero a veces, si los tipos son bastante grandes, puede ser de gran ayuda (sobre todo en evitar largos tiempos de construcción)

14

Si su clase utiliza el idioma 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 se cambia el archivo de cabecera de la clase externa, usted tiene que volver a compilar todo lo que #Includes (y si alguno de los que están los archivos de cabecera, usted tiene que volver a compilar todo lo que les #Includes, y así sucesivamente)

0

no sé si esto es una diferencia vale la pena mencionar, pero ...

¿sería posible tener la aplicación en su propio espacio de nombres y tienen un espacio de nombres envoltorio pública/biblioteca para el código que ve el usuario:

catlib::Cat::Purr(){ cat_->Purr(); } 
cat::Cat::Purr(){ 
    printf("purrrrrr"); 
} 

De esta manera, todas las lib el código de alerta puede hacer uso del espacio de nombres de gato y, a medida que surge la necesidad de exponer una clase al usuario, se puede crear un contenedor en el espacio de nombres de catlib.

0

Me parece revelador que, a pesar de lo conocido que es el idioma pimpl, no lo veo aparecer 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 si hay alguna necesidad de que su implementación sea que esté bien oculta, y quizás sea bastante raro que la gente realmente cambie solo la implementación; tan pronto como necesite agregar nuevos métodos, digamos, necesita cambiar el encabezado de todos modos.

+1

Sí, solo hace la diferencia si puede elegir una buena interfaz pública y atenerse a ella. Como menciona Rob Wells, esto es importante si necesita distribuir bibliotecas actualizadas a personas vinculadas con versiones compiladas de su biblioteca sin obligarlas a recompilar; solo debe proporcionar una nueva DLL y listo. Eso sí, usar interfaces (== clases abstractas sin miembros de datos en C++) logra casi lo mismo con menos mantenimiento (no es necesario reenviar manualmente cada método público). Pero OTOH debe usar una sintaxis especial para crear instancias (es decir, invocar un método de fábrica). –

+4

Qt usa la expresión idiomática PIMPL extensivamente. –

1

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 volviera a afectarme.

Para responder a la pregunta original, la ventaja que encontré al reenviar las llamadas a la clase de pimpl era que la clase pimpl es igual a la que habrían sido las clases originales antes de que lo pellizcasen, además de sus implementaciones aren ' t se extendió en 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 el Sr. Nodet dijo, una clase, una responsabilidad.

3

Bueno, yo 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 la expresión pImpl.

+2

Si ya se está limitando a un enfoque de fábrica para la creación de objetos, entonces está bien. Pero elimina completamente la semántica de valores, mientras que un pImpl tradicional funciona con cualquier método. –

+2

Well pImpl básicamente solo envuelve el puntero. Todo lo que necesita hacer es hacer que el create() anterior devuelva un PointerWrapperWithCopySemantics :-). Normalmente hago el opuesto, y devuelvo un std :: auto_ptr . –

+10

¿Por qué la herencia sobre la composición? ¿Por qué agregar la sobrecarga de un vtable? ¿Por qué herencia virtual? – derpface

17

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é un usuario que solo quiere usar el Cat tiene que incluir todo eso? Por lo tanto, todos los archivos necesarios se ocultan mediante 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 (léase "muchas matemáticas desagradables"), que se implementa en plantillas, por lo que la mayor parte 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 que de otro modo estaría vacío tarda aproximadamente 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 tiene necesariamente que ver con la protección de la implementación para que no sea copiada por otras compañías, lo que probablemente no ocurriría de todos modos, a menos que el funcionamiento interno de su algoritmo pueda deducirse de las definiciones de las variables miembro (si entonces, probablemente no es muy complicado y no vale la pena protegerlo en primer lugar).

1

Utilizamos la terminología PIMPL para emular la programación orientada a aspectos donde se invocan los aspectos de pre, post y error antes y después de la ejecución de una función de miembro.

struct Omg{ 
    void purr(){ cout<< "purr\n"; } 
}; 

struct Lol{ 
    Omg* omg; 
    /*...*/ 
    void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } } 
}; 

También utilizamos el 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.

Cuestiones relacionadas