Lengua Pimpl vs Interfaz de clase virtual pura

Me preguntaba qué haría que un progtwigdor escogiera ya sea el modismo Pimpl o la clase virtual pura y la herencia.

Entiendo que el idioma de pimpl viene con una indirección extra explícita para cada método público y la sobrecarga de creación de objetos.

La clase virtual pura, por otro lado, viene con indirección implícita (vtable) para la implementación heredada y entiendo que no hay sobrecarga de creación de objetos.
EDITAR : Pero necesitarías una fábrica si creas el objeto desde el exterior

¿Qué hace que la clase virtual pura sea menos deseable que la frase de pimpl?

Al escribir una clase de C ++, es apropiado pensar si va a ser

  1. Un tipo de valor

    Copia por valor, la identidad nunca es importante. Es apropiado que sea una clave en un estándar :: mapa. Ejemplo, una clase de “cadena”, una clase de “fecha” o una clase de “número complejo”. Para “copiar” instancias de dicha clase tiene sentido.

  2. Un tipo de entidad

    La identidad es importante. Siempre pasado por referencia, nunca por “valor”. A menudo, no tiene sentido “copiar” instancias de la clase en absoluto. Cuando tiene sentido, un método polimórfico “Clonar” suele ser más apropiado. Ejemplos: una clase de socket, una clase de base de datos, una clase de “política”, cualquier cosa que sería un “cierre” en un lenguaje funcional.

Tanto pImpl como la clase base abstracta pura son técnicas para reducir las dependencias de tiempo de comstackción.

Sin embargo, solo uso pImpl para implementar tipos de valor (tipo 1), y solo algunas veces cuando realmente quiero minimizar el acoplamiento y las dependencias en tiempo de comstackción. A menudo, no vale la pena la molestia. Como bien lo señala, hay más gastos indirectos sintácticos porque tiene que escribir métodos de reenvío para todos los métodos públicos. Para las clases de tipo 2, siempre utilizo una clase base abstracta pura con método (s) de fábrica asociados.

Pointer to implementation generalmente trata de ocultar detalles de implementación estructural. Interfaces tratan de instancias de diferentes implementaciones. Realmente sirven dos propósitos diferentes.

El idioma pimpl te ayuda a reducir las dependencias y los tiempos de comstackción, especialmente en aplicaciones grandes, y minimiza la exposición del encabezado de los detalles de implementación de tu clase a una unidad de comstackción. Los usuarios de su clase ni siquiera deberían estar al tanto de la existencia de un grano (¡excepto como un puntero críptico al que no tienen acceso!).

Las clases abstractas (virtuales puros) es algo de lo que sus clientes deben estar conscientes: si trata de usarlas para reducir el acoplamiento y las referencias circulares, debe agregar alguna forma de permitirles crear sus objetos (por ejemplo, mediante métodos o clases de fábrica, dependency injection u otros mecanismos).

Estaba buscando una respuesta para la misma pregunta. Después de leer algunos artículos y practicar , prefiero usar “Interfaces de clases virtuales puras” .

  1. Son más directos (esta es una opinión subjetiva). La expresión Pimpl me hace sentir que estoy escribiendo código “para el comstackdor”, no para el “próximo desarrollador” que leerá mi código.
  2. Algunos frameworks de prueba tienen soporte directo para burlarse de clases virtuales puras
  3. Es cierto que necesita una fábrica para ser accesible desde el exterior. Pero si quiere aprovechar el polymorphism: eso también es “pro”, no es una “estafa”. … y un simple método de fábrica no duele tanto

El único inconveniente ( estoy tratando de investigar sobre esto ) es que el modismo de pimpl podría ser más rápido

  1. cuando las llamadas proxy están en línea, mientras que heredan necesariamente necesitan un acceso adicional al objeto VTABLE en tiempo de ejecución
  2. la huella de memoria la clase proxy-public del pimpl es más pequeña (puede hacer optimizaciones fácilmente para intercambios más rápidos y otras optimizaciones similares)

Hay un problema muy real con las bibliotecas compartidas que el idioma pimpl elude limpiamente que los virtuales puros no pueden: no se puede modificar / eliminar con seguridad los miembros de datos de una clase sin forzar a los usuarios de la clase a recomstackr su código. Eso puede ser aceptable en algunas circunstancias, pero no, por ejemplo, para las bibliotecas del sistema.

Para explicar el problema en detalle, considere el siguiente código en su biblioteca / encabezado compartido:

 // header struct A { public: A(); // more public interface, some of which uses the int below private: int a; }; // library A::A() : a(0) {} 

El comstackdor emite código en la biblioteca compartida que calcula la dirección del entero que se inicializará para que sea un cierto desplazamiento (probablemente cero en este caso, porque es el único miembro) del puntero al objeto A que sabe que es this .

En el lado del usuario del código, un new A primero asignará el sizeof(A) bytes de memoria, y luego le dará un puntero a esa memoria al constructor A::A() .

Si en una revisión posterior de su biblioteca decide eliminar el número entero, hacerlo más grande, más pequeño o agregar miembros, habrá una discrepancia entre la cantidad de memoria asignada por el código del usuario y las compensaciones que espera el código del constructor. El resultado probable es un locking, si tiene suerte; si tiene menos suerte, su software se comporta de manera extraña.

Mediante pimpl’ing, puede agregar y eliminar miembros de datos de forma segura a la clase interna, ya que la asignación de memoria y la llamada al constructor ocurren en la biblioteca compartida:

 // header struct A { public: A(); // more public interface, all of which delegates to the impl private: void * impl; }; // library A::A() : impl(new A_impl()) {} 

Todo lo que necesita hacer ahora es mantener su interfaz pública libre de miembros de datos que no sean el puntero al objeto de implementación, y está a salvo de esta clase de errores.

Editar: Tal vez debería añadir que la única razón por la que estoy hablando del constructor aquí es que no quería proporcionar más código; la misma argumentación se aplica a todas las funciones que acceden a miembros de datos.

¡Odio los granos! Hacen la clase fea y no legible. Todos los métodos son redirigidos a la espinilla. Nunca se ve en los encabezados, qué funcionalidades tiene la clase, por lo que no se puede refactorizar (por ejemplo, simplemente cambiar la visibilidad de un método). La clase se siente como “embarazada”. Creo que el uso de iterfaces es mejor y más que suficiente para ocultar la implementación del cliente. Puede permitir que una clase implemente varias interfaces para mantenerlas delgadas. ¡Uno debería preferir las interfaces! Nota: No es necesario que necesite la clase de fábrica. Lo relevante es que los clientes de la clase se comunican con sus instancias a través de la interfaz adecuada. La ocultación de métodos privados me parece una extraña paranoia y no veo razón para esto, ya que tenemos interfaces.

No debemos olvidar que la herencia es un acoplamiento más fuerte y más cercano que la delegación. También tomaría en cuenta todas las cuestiones planteadas en las respuestas dadas al decidir qué modismos de diseño emplear para resolver un problema en particular.

Según entiendo, estas dos cosas tienen propósitos completamente diferentes. El propósito del idioma de la espinilla es, básicamente, darle un manejo a su implementación para que pueda hacer cosas como intercambios rápidos de algún tipo.

El propósito de las clases virtuales es más a lo largo de la línea de permitir el polymorphism, es decir, tienes un puntero desconocido para un objeto de un tipo derivado y cuando llamas a la función x siempre obtienes la función correcta para cualquier clase a la que apunta el puntero base.

Manzanas y naranjas realmente.

Aunque ampliamente cubierto en las otras respuestas, tal vez pueda ser un poco más explícito sobre un beneficio de pimpl sobre las clases base virtuales:

Un enfoque pimpl es transparente desde el punto de vista del usuario, lo que significa que puede, por ejemplo, crear objetos de la clase en la stack y usarlos directamente en contenedores. Si intenta ocultar la implementación utilizando una clase base virtual abstracta, deberá devolver un puntero compartido a la clase base de una fábrica, lo que complicará su uso. Considere el siguiente código de cliente equivalente:

 // Pimpl Object pi_obj(10); std::cout << pi_obj.SomeFun1(); std::vector objs; objs.emplace_back(3); objs.emplace_back(4); objs.emplace_back(5); for (auto& o : objs) std::cout << o.SomeFun1(); // Abstract Base Class auto abc_obj = ObjectABC::CreateObject(20); std::cout << abc_obj->SomeFun1(); std::vector> objs2; objs2.push_back(ObjectABC::CreateObject(13)); objs2.push_back(ObjectABC::CreateObject(14)); objs2.push_back(ObjectABC::CreateObject(15)); for (auto& o : objs2) std::cout << o->SomeFun1(); 

El problema más molesto sobre el idioma pimpl es que hace extremadamente difícil mantener y analizar el código existente. Así que al usar pimpl se paga con tiempo de desarrollador y frustración solo para “reducir las dependencias y tiempos de comstackción y minimizar la exposición del encabezado de los detalles de implementación”. Decídete a ti mismo, si realmente vale la pena.

Especialmente los “tiempos de construcción” son un problema que puede resolver con un hardware mejor o usando herramientas como Incredibuild (www.incredibuild.com), sin afectar su diseño de software. El diseño del software generalmente debe ser independiente de la forma en que se crea el software.

    Intereting Posts