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

49

Veo un montón de código fuente que usa la expresión de PImpl en C ++. Supongo que su propósito es ocultar los datos / tipo / implementación privados, 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 interface / pure-abstract en C ++ también tienen esta capacidad, también se pueden usar para ocultar datos / tipo / implementación. Y para permitir 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 más bajo, ya que ni siquiera necesita repetir la implementación de la función de contenedor público void Bar::doWork() { return m_impl->doWork(); }, solo necesita definir la firma en la interfaz.

  2. Bien entendido :

    La tecnología de interfaz es mejor entendida por todos los desarrolladores de C ++.

  3. Rendimiento :

    El rendimiento de la interfaz no es peor que el idioma de PImpl, tanto un acceso a memoria adicional. Supongo que el rendimiento es el mismo.

El siguiente es 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 usando 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?

ZijingWu
fuente

Respuestas:

50

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, eso todavía implementa interfaces y anula los métodos virtuales de la clase base, etc. Entonces, la implementación más simple de PImpl no es una interfaz, ¡es una clase simple que contiene directamente a los miembros!

Hay tres razones para usar PImpl:

  1. Hacer que la interfaz binaria (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 binaria permanezca igual. Ahora, casi cualquier cambio en el encabezado, excepto agregar una función no miembro y agregar una función miembro no virtual, cambia la ABI. El idioma de PImpl mueve la definición de los miembros privados a la fuente y, por lo tanto, desacopla el ABI de su definición. Ver problema de interfaz binaria frágil

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

  3. Para muchas clases en excepciones de C ++, la seguridad es una propiedad importante. A menudo necesita 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 estado inconsistente si lanza y necesita que el objeto que contiene permanezca consistente. En tal caso, implemente la operación creando una nueva instancia de PImpl y cámbielos cuando la operación tenga éxito.

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

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

  2. La interfaz solo se puede usar a través de puntero / referencia, por lo que el usuario debe ocuparse de la gestión adecuada de los recursos. Por otro lado, las clases que usan PImpl siguen siendo tipos de valores y manejan los recursos internamente.

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

Y, por supuesto, la interfaz no ayudará con la seguridad de excepción. Necesitas la indirección dentro de la clase para eso.

Jan Hudec
fuente
Buen punto. Añadir la función pública en Implla clase no rompa el ABI, además de añadir la función pública en la interfaz se romperá ABI.
ZijingWu
1
Agregar una función / método público no tiene que romper el ABI; Los cambios estrictamente aditivos no están rompiendo los cambios. Sin embargo, debes ser muy cuidadoso; El código que media entre el front-end y el back-end tiene que lidiar con todo tipo de diversión con el control de versiones. Todo se pone complicado. (Este tipo de cosas es por qué un número de proyectos de software prefieren C a C ++, sino que les permite obtener un control más estricto sobre exactamente lo que el ABI es.)
Donal Fellows
3
@DonalFellows: Agregar una función / método público no interrumpe la ABI. Agregar un método virtual lo hace y lo hace siempre, a menos que se impida al usuario heredar el objeto (que PImpl logra).
Jan Hudec
44
Para aclarar por qué agregar un método virtual rompe el ABI. Tiene que ver con el enlace. Vincular a métodos no virtuales es por nombre, independientemente de si la biblioteca es estática o dinámica. Agregar otro método no virtual es simplemente agregar otro nombre para vincular. Sin embargo, los métodos virtuales son índices en una tabla virtual, y el código externo se vincula efectivamente por índice. Agregar un método virtual puede mover otros métodos en la tabla virtual sin que el código externo sepa de ninguna manera.
SnakE
1
PImpl también reduce el tiempo de compilación inicial. Por ejemplo, si mi aplicación tiene un campo de algún tipo de plantilla (por ejemplo unordered_set), sin PImpl, necesito #include <unordered_set>en MyClass.h. Con PImpl, solo necesito incluirlo en el .cpparchivo, no en el .harchivo, y por lo tanto, todo lo demás que lo incluye ...
Martin C. Martin
7

Solo quiero abordar su punto de rendimiento. Si usa una interfaz, necesariamente ha creado funciones virtuales que NO serán alineadas 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 de análisis de programa completas que tardan mucho tiempo en realizarse.

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

Chemball masticable
fuente
1
Con el uso de la optimización del tiempo de enlace, se elimina la mayor parte de la sobrecarga de usar pimpl idiom. Tanto las funciones de envoltura externa como las funciones de implementación pueden integrarse, lo que hace que la función llame de manera efectiva como una clase normal implementada dentro de un encabezado.
Goji
Esto solo es cierto si utiliza la optimización del tiempo de enlace. El punto de PImpl es que el compilador no tiene la implementación, ni siquiera la función de reenvío. Sin embargo, el enlazador sí.
Martin C. Martin
5

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

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

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

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

Dave Hillier
fuente
PImpl no es una optimización en el tiempo de ejecución en absoluto. Las asignaciones no son triviales y pimpl significa asignación adicional. Es una técnica de ruptura de dependencia y optimización del tiempo de compilación solamente.
Jan Hudec
@JanHudec aclaró.
Dave Hillier
@DaveHillier, +1 por mencionar FragileBinaryInterfaceProblem
ZijingWu