c++ - El idioma Pimpl en la práctica
pimpl-idiom (8)
Ha habido algunas preguntas sobre SO sobre el lenguaje pimpl , pero tengo más curiosidad acerca de la frecuencia con que se aprovecha en la práctica.
Entiendo que hay algunas compensaciones entre el rendimiento y la encapsulación, además de algunas molestias de depuración debido a la redirección adicional.
Con eso, ¿es esto algo que debería ser adoptado en una base por clase o de todo o nada? ¿Es esta una mejor práctica o preferencia personal?
Me doy cuenta de que es algo subjetivo, así que permítame enumerar mis prioridades principales:
- Claridad de código
- Mantenimiento de código
- Actuación
Siempre asumo que tendré que exponer mi código como una biblioteca en algún momento, por lo que también es una consideración.
EDITAR: Cualquier otra opción para lograr lo mismo sería bienvenido sugerencias.
Claridad del código
La claridad del código es muy subjetiva, pero en mi opinión, un encabezado que tiene un solo miembro de datos es mucho más legible que un encabezado con muchos miembros de datos. Sin embargo, el archivo de implementación es más ruidoso, por lo que la claridad se reduce allí. Eso podría no ser un problema si la clase es una clase base, utilizada principalmente por clases derivadas en lugar de mantenerse.
Mantenibilidad
Para el mantenimiento de la clase pimpl''d, personalmente encuentro la desreferencia extra en cada acceso de un miembro de datos tedioso. Los accesores no pueden ayudar si los datos son puramente privados porque, de todos modos, no deberías exponer a un accesor o mutador, y te quedas estancado por el hecho de estar desreferiéndolo constantemente al pimpl.
Para mantener la capacidad de las clases derivadas, encuentro que el idioma es una ganancia pura en todos los casos, porque el archivo de encabezado enumera menos detalles irrelevantes. El tiempo de compilación también se mejora para todas las unidades de compilación del cliente.
Actuación
La pérdida de rendimiento es pequeña en muchos casos y significativa en pocos. A largo plazo, está en el orden de magnitud de la pérdida de rendimiento de las funciones virtuales. Estamos hablando de una desreferencia adicional por acceso por miembro de datos, más asignación dinámica de memoria para el pimpl, más liberación de la memoria en la destrucción. Si la clase pimpl''d no accede a sus miembros de datos a menudo, los objetos de la clase pimpl''d se crean a menudo y son de corta duración, por lo que la asignación dinámica puede superar las extra dereferencias.
Decisión
Creo que las clases en las que el rendimiento es crucial, de modo que una falta de referencia adicional o la asignación de memoria hacen una diferencia significativa, no deberían usar el pimpl sin importar qué. La clasificación básica en la que esta reducción en el rendimiento es insignificante y de la cual el archivo de encabezado está ampliamente incluido probablemente debería usar el pimpl si se mejora significativamente el tiempo de compilación. Si el tiempo de compilación no se reduce se reduce a su gusto de claridad de código.
Para todos los demás casos es puramente una cuestión de gustos. Pruébelo y mida el rendimiento en tiempo de ejecución y el rendimiento en tiempo de compilación antes de tomar una decisión.
Este idioma ayuda enormemente con el tiempo de compilación en grandes proyectos.
Generalmente lo uso cuando quiero evitar que un archivo de encabezado contamine mi código base. Windows.h es el ejemplo perfecto. Se comporta tan mal que preferiría suicidarme antes que tenerlo visible en todas partes. Asumiendo que quieres una API basada en clase, ocultándola detrás de una clase pimpl resuelve el problema de forma ordenada. (Si está contento con solo exponer una función individual, esas pueden ser simplemente declaradas hacia adelante, por supuesto, sin colocarlas en una clase de pimpl)
No usaría pimpl en todas partes , en parte debido al impacto en el rendimiento, y en parte solo porque es mucho trabajo extra para un beneficio generalmente pequeño. Lo principal que le proporciona es el aislamiento entre la implementación y la interfaz. Por lo general, eso no es una prioridad muy alta.
Uno de los usos más importantes de pimpl ideom es la creación de C ++ ABI estable. Casi todas las clases de Qt usan el puntero "D" que es una especie de grano. Esto permite realizar cambios mucho más fáciles sin romper ABI.
Utilizo el idioma en un par de lugares en mis propias bibliotecas, en ambos casos para separar limpiamente la interfaz de la implementación. Tengo, por ejemplo, una clase de lector XML totalmente declarada en un archivo .h, que tiene un PIMPL a una clase RealXMLReader que se declara y define en archivos .h y .cpp no públicos. El RealXMlReader a su vez es un envoltorio de conveniencia para el analizador XML que uso (actualmente Expat).
Este arreglo me permite cambiar de Expat en el futuro a otro analizador XML sin tener que volver a compilar todo el código del cliente (todavía necesito volver a vincularlo).
Tenga en cuenta que no hago esto por razones de rendimiento en tiempo de compilación, solo por conveniencia. Hay algunos geniales de PIMPL que insisten en que cualquier proyecto que contenga más de tres archivos no será compilable a menos que use PIMPL en todo momento. Es evidente que estas personas nunca producen ninguna evidencia real, sino que solo hacen referencias vagas a "Latkos" y "tiempo exponencial".
Yo diría que si lo haces por clase o sobre una base de todo o nada, depende de por qué eliges el idioma de los páppl en primer lugar. Mis razones, al construir una biblioteca, han sido una de las siguientes:
- Quería ocultar la implementación para evitar revelar información (sí, no era un proyecto de FOSS :)
- Quería ocultar la implementación para hacer que el código del cliente sea menos dependiente. Si creas una biblioteca compartida (DLL), puedes cambiar tu clase pimpl sin siquiera compilar la aplicación.
- Quería reducir el tiempo que toma compilar las clases usando la biblioteca.
- Quería arreglar un choque de espacio de nombres (o similar).
Ninguna de estas razones solicita el enfoque de todo o nada. En el primero, solo hace un análisis detallado de lo que quiere ocultar, mientras que en el segundo caso es probable que sea suficiente para las clases que espera cambiar. También por la tercera y cuarta razón, solo hay beneficios al ocultar miembros no triviales que a su vez requieren encabezados adicionales (por ejemplo, de una biblioteca de terceros, o incluso STL).
En cualquier caso, mi punto es que, por lo general, no encontraría algo así como demasiado útil:
class Point {
public:
Point(double x, double y);
Point(const Point& src);
~Point();
Point& operator= (const Point& rhs);
void setX(double x);
void setY(double y);
double getX() const;
double getY() const;
private:
class PointImpl;
PointImpl* pimpl;
}
En este tipo de caso, la compensación comienza a golpearlo porque el puntero debe ser referenciado, y los métodos no pueden estar en línea. Sin embargo, si lo hace solo para clases no triviales, la ligera sobrecarga generalmente se puede tolerar sin ningún problema.
pImpl es muy útil cuando viene a implementar std :: swap y operator = con la garantía de excepción sólida. Me inclino a decir que si su clase es compatible con alguno de esos, y tiene más de un campo no trivial, por lo general ya no es preferible.
De lo contrario, se trata de cuán firmemente desea que los clientes estén vinculados a la implementación a través del archivo de encabezado. Si los cambios incompatibles con los binarios no son un problema, entonces es posible que no se beneficie mucho de la capacidad de mantenimiento, aunque si la velocidad de compilación se convierte en un problema, generalmente hay ahorros allí.
Los costos de rendimiento probablemente tienen más que ver con la pérdida de alineación que con la indirección, pero eso es una suposición descabellada.
Siempre puede agregar pImpl más tarde y declarar que a partir de este día los clientes no tendrán que volver a compilar solo porque agregó un campo privado.
Así que nada de esto sugiere un enfoque de todo o nada. Puede hacerlo de forma selectiva para las clases en las que le brinda beneficios, no para las que no lo hacen, y cambiar de opinión más adelante. Implementar por ejemplo iteradores como pImpl suena como Too Much Design ...
pImpl funcionará mejor cuando tengamos semántica de valor r.
La "alternativa" a pImpl, que también logrará ocultar el detalle de la implementación, es usar una clase base abstracta y colocar la implementación en una clase derivada. Los usuarios llaman a algún tipo de método "de fábrica" para crear la instancia y generalmente usarán un puntero (probablemente uno compartido) para la clase abstracta.
La razón detrás de pImpl en cambio puede ser:
- Ahorrar en una v-table. Sí, pero su compilador incluirá todo el reenvío y realmente guardará algo.
- Si su módulo contiene varias clases que se conocen entre sí en detalle, aunque al mundo exterior se lo oculta.
La semántica de la clase contenedora para el pImpl podría ser: - No se puede copiar, no se puede asignar ... Así que usted "nuevo" su pImpl en la construcción y "eliminar" en la destrucción - compartido. Así que has compartido_ptr en lugar de Impl *
Con shared_ptr puede usar una declaración de reenvío siempre que la clase esté completa en el punto del destructor. Tu destructor debe definirse incluso si está predeterminado (lo cual probablemente será).
intercambiable Puede implementar "puede estar vacío" e implementa "swap". Los usuarios pueden crear una instancia de uno y pasarle una referencia no constante para que se complete, con un "intercambio".
Construcción de 2 etapas. Usted construye uno vacío y luego llama "load ()" para rellenarlo.
compartido es el único que tengo incluso un gusto remoto por sin semántica de valor r. Con ellos también podemos implementar correctamente no asignables no copiables. Me gusta poder llamar a una función que me da una.
Sin embargo, he encontrado que ahora tiendo más a usar clases base abstractas más que pImpl, incluso cuando solo hay una implementación.