¿Cuál es el punto del patrón PImpl mientras podemos usar la interfaz para el mismo propósito en C ++?

44

Veo un montón de código fuente que utiliza el lenguaje PImpl en C ++. Supongo que su propósito es ocultar los datos privados / tipo / implementación, para que pueda eliminar la dependencia, y luego reducir el tiempo de compilación y el problema de inclusión del encabezado.

Pero las clases de interfaz / extracto puro en C ++ también tienen esta capacidad, también pueden usarse para ocultar datos / tipo / implementación. Y para que la persona que llama solo vea la interfaz al crear un objeto, podemos declarar un método de fábrica en el encabezado de la interfaz.

La comparación es:

  1. Coste :

    El costo de la interfaz es menor, ya que ni siquiera necesita repetir la implementación de la función de envoltura pública void Bar::doWork() { return m_impl->doWork(); } , solo necesita definir la firma en la interfaz.

  2. Bien entendido :

    Todos los desarrolladores de C ++ comprenden mejor la tecnología de interfaz.

  3. Rendimiento :

    El rendimiento de la interfaz no es peor que el lenguaje PImpl, tanto un acceso de memoria adicional. Asumo que el rendimiento es el mismo.

A continuación se muestra el pseudocódigo para ilustrar mi pregunta:

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

El mismo propósito se puede implementar utilizando la interfaz:

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

Entonces, ¿cuál es el punto de PImpl?

    
pregunta ZijingWu 03.10.2013 - 13:00

3 respuestas

42

Primero, PImpl se usa generalmente para clases no polimórficas. Y cuando una clase polimórfica tiene PImpl, por lo general sigue siendo polimórfica, que aún implementa interfaces y reemplaza los métodos virtuales de la clase base y así sucesivamente. Así que la implementación más simple de PImpl es la interfaz no , ¡es una clase simple que contiene directamente a los miembros!

Hay tres razones para usar PImpl:

  1. Hacer que la interfaz binary (ABI) sea independiente de los miembros privados. Es posible actualizar una biblioteca compartida sin volver a compilar el código dependiente, pero solo mientras la interfaz binary permanezca igual. Ahora, casi cualquier cambio en el encabezado, excepto la adición de una función no miembro y la adición de una función miembro no virtual, cambia la ABI. El lenguaje PImpl mueve la definición de los miembros privados a la fuente y, por lo tanto, desacopla el ABI de su definición. Consulte Problema de interfaz binaria frágil

  2. Cuando un encabezado cambia, todas las fuentes que lo incluyan deben ser recompiladas. Y la compilación de C ++ es bastante lenta. Por lo tanto, al mover las definiciones de los miembros privados a la fuente, el lenguaje PImpl reduce el tiempo de compilación, ya que se necesitan menos dependencias en el encabezado, y reduce el tiempo de compilación después de las modificaciones aún más, ya que los dependientes no necesitan ser recompilados. (ok, esto también se aplica a la interfaz + función de fábrica con clase concreta oculta).

  3. Para muchas clases en C ++, la seguridad es una propiedad importante. A menudo es necesario componer varias clases en una, de modo que si durante la operación en más de un miembro lanza, ninguno de los miembros se modifica o tiene una operación que dejará al miembro en un estado inconsistente si se lanza y necesita que el objeto contenedor permanezca consistente. En tal caso, implementa la operación al crear una nueva instancia de PImpl e intercambiarlas cuando la operación sea exitosa.

En realidad, la interfaz también se puede utilizar para ocultar la implementación solamente, pero tiene las siguientes desventajas:

  1. Agregar método no virtual no rompe ABI, pero sí agregar uno virtual. Por lo tanto, las interfaces no permiten agregar métodos, PImpl sí.

  2. Inteface solo se puede utilizar mediante puntero / referencia, por lo que el usuario debe ocuparse de la administración adecuada de los recursos. Por otro lado, las clases que usan PImpl son todavía tipos de valor y manejan los recursos internamente.

  3. La implementación oculta no se puede heredar, la clase con PImpl puede.

Y, por supuesto, la interfaz no ayuda con la excepción de seguridad. Necesitas la dirección dentro de la clase para eso.

    
respondido por el Jan Hudec 03.10.2013 - 13:17
6

Solo quiero abordar tu punto de rendimiento. Si usa una interfaz, necesariamente ha creado funciones virtuales que NO serán integradas por el optimizador del compilador. Las funciones PIMPL pueden (y probablemente lo harán, porque son muy cortas) estar en línea (quizás más de una vez si la función IMPL también es pequeña). Una llamada de función virtual no se puede optimizar a menos que use optimizaciones completas de análisis de programas, lo cual lleva mucho tiempo.

Si su clase PIMPL no se usa de manera crítica para el rendimiento, este punto no importa mucho, pero su suposición de que el rendimiento es el mismo solo se cumple en algunas situaciones, no en todas.

    
respondido por el Chewy Gumball 24.10.2013 - 00:06
5

Esta página responde a su pregunta. Muchas personas están de acuerdo con su afirmación. Usar Pimpl tiene las siguientes ventajas sobre los campos / miembros privados.

  
  1. Cambiar las variables de miembros privados de una clase no requiere volver a compilar las clases que dependen de ella, por lo que los tiempos son más rápidos, y   El FragileBinaryInterfaceProblem se reduce.
  2.   
  3. El archivo de encabezado no necesita #incluir las clases que se usan 'por valor' en las variables de miembros privados, por lo que los tiempos de compilación son más rápidos.
  4.   

El lenguaje Pimpl es una optimización en tiempo de compilación, una técnica de ruptura de dependencias. Se reduce archivos de encabezado grandes. Reduce su encabezado solo a la interfaz pública. La longitud del encabezado es importante en C ++ cuando piensas en cómo funciona. #include concatena efectivamente el archivo de encabezado al archivo fuente, así que tenga en cuenta que las unidades C ++ preprocesadas pueden ser muy grandes. Usar Pimpl puede mejorar los tiempos de compilación.

Hay otras mejores técnicas para romper la dependencia, que incluyen mejorar su diseño. ¿Qué representan los métodos privados? ¿Cuál es la única responsabilidad? ¿Deberían ser otra clase?

    
respondido por el Dave Hillier 03.10.2013 - 13:16

Lea otras preguntas en las etiquetas